@takazudo/zudo-doc 0.2.0 → 0.2.2
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 +53 -2
- package/dist/doclayout/doc-layout.js +25 -19
- package/dist/header/nav-active.d.ts +0 -11
- package/dist/header/nav-active.js +7 -2
- package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +13 -0
- package/dist/html-preview-wrapper/html-preview-wrapper.js +12 -1
- package/dist/html-preview-wrapper/html-preview.d.ts +43 -2
- package/dist/html-preview-wrapper/html-preview.js +8 -2
- package/dist/integrations/claude-resources/generate.js +50 -54
- package/dist/integrations/doc-history/pre-build.d.ts +13 -5
- package/dist/integrations/doc-history/pre-build.js +11 -28
- package/dist/integrations/llms-txt/dev-middleware.d.ts +1 -0
- package/dist/integrations/llms-txt/emit.d.ts +1 -0
- package/dist/integrations/llms-txt/generate.d.ts +1 -0
- package/dist/integrations/llms-txt/index.d.ts +3 -2
- package/dist/integrations/llms-txt/load.d.ts +7 -35
- package/dist/integrations/llms-txt/load.js +21 -54
- package/dist/integrations/llms-txt/strip.d.ts +11 -9
- package/dist/integrations/llms-txt/strip.js +1 -3
- package/dist/integrations/llms-txt/types.d.ts +7 -20
- package/dist/integrations/search-index/collect.js +3 -2
- package/dist/md-utils/index.d.ts +76 -0
- package/dist/{integrations/search-index/content-files.js → md-utils/index.js} +12 -5
- package/dist/safelist.css +1 -1
- package/dist/sidebar-tree/category-meta.js +6 -1
- package/dist/theme/iframe-bridge.js +5 -3
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/theme-toggle.d.ts +7 -6
- package/dist/theme/theme-toggle.js +5 -81
- package/dist/theme-toggle/color-scheme-sync.d.ts +38 -0
- package/dist/theme-toggle/color-scheme-sync.js +22 -0
- package/dist/theme-toggle/index.d.ts +12 -0
- package/dist/theme-toggle/index.js +83 -0
- package/package.json +15 -6
- package/dist/integrations/search-index/content-files.d.ts +0 -42
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ This package provides the missing-by-design framework concerns:
|
|
|
6
6
|
|
|
7
7
|
- **Sidebar tree builder** (`./sidebar-tree`) — turns collection entries + `_category_.json` into a sidebar `SidebarNode[]`.
|
|
8
8
|
- **Theme controls** (`./theme`) — color scheme provider + design-token tweak panel (Preact island that wraps an iframe).
|
|
9
|
+
- **Theme toggle (bare)** (`./theme-toggle`) — the un-wrapped ThemeToggle component for call sites that compose their own `<Island>` (the `./theme` barrel exports an Island-wrapped variant of the same component).
|
|
9
10
|
- **TOC** (`./toc`) — desktop and mobile TOC Preact islands fed by MDX `headings` export.
|
|
10
11
|
- **Breadcrumb** (`./breadcrumb`) — JSX breadcrumb fed by the sidebar tree.
|
|
11
12
|
- **DocLayout** (`./doclayout`) — composable layout shell with explicit `<Header>`, `<Sidebar>`, `<Main>`, `<Toc>`, `<Footer>` props; ships a `<DocLayoutWithDefaults>` wrapper that holds the 16 `create-zudo-doc` injection anchors.
|
|
@@ -13,6 +14,56 @@ This package provides the missing-by-design framework concerns:
|
|
|
13
14
|
- **Head injection** (`./head`) — canonical, og:\*, twitter:\*, robots, preload hints, RSS link, sitemap link, theme-color — byte-equal to today's legacy doc-layout output.
|
|
14
15
|
- **SSR-skip wrappers** (`./ssr-skip`) — `<AiChatModalIsland>`, `<ImageEnlargeIsland>`, `<DesignTokenTweakPanelIsland>`, `<MockInitIsland>` — wrap zfb's `<Island ssrFallback>` with the right fallback markup so doc pages don't have to re-implement the SSR-skip pattern.
|
|
15
16
|
|
|
16
|
-
##
|
|
17
|
+
## Optional peer dependency: shiki
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
`./html-preview-wrapper`'s `<HighlightedCode>` lazily `import("shiki")`s at runtime for client-side syntax highlighting. `shiki` is declared as an **optional peerDependency** — install it in your project if you use that subpath:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add shiki
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Projects scaffolded by `create-zudo-doc` already include it. If you never render `<HtmlPreview>` / `<HighlightedCode>`, you can omit it.
|
|
26
|
+
|
|
27
|
+
## ⚠️ HTML preview iframe sandbox — trust assumption
|
|
28
|
+
|
|
29
|
+
`<HtmlPreview>` / `<HtmlPreviewWrapper>` render their preview inside an `<iframe srcdoc>` whose `sandbox` attribute **defaults** to:
|
|
30
|
+
|
|
31
|
+
- `allow-scripts allow-same-origin` when the preview contains scripts (a `js` prop or a `<script>` in `head`), or
|
|
32
|
+
- `allow-same-origin` when it does not.
|
|
33
|
+
|
|
34
|
+
**`allow-scripts` + `allow-same-origin` together effectively void the sandbox** — scripts running inside the preview share the parent page's origin and can reach the parent document. This default is intentional and safe for zudo-doc's own use case, where preview content is **author-trusted** MDX. The `allow-same-origin` token is what lets the component auto-measure the iframe body and sync its height.
|
|
35
|
+
|
|
36
|
+
If your project renders **semi-trusted or user-submitted** HTML in a preview, override the sandbox with a stricter value via the `sandbox` prop:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Maximally restrictive — no script execution, opaque origin
|
|
40
|
+
<HtmlPreviewWrapper html={untrustedHtml} sandbox="" height={400} />
|
|
41
|
+
|
|
42
|
+
// Allow scripts but keep an opaque origin (script can't reach the parent)
|
|
43
|
+
<HtmlPreviewWrapper html={untrustedHtml} sandbox="allow-scripts" height={400} />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Caveat:** removing `allow-same-origin` gives the iframe an opaque origin, which blocks the parent from reading `iframe.contentDocument`. That **disables auto-height** — always pair a stricter `sandbox` with a fixed `height`. Passing the empty string `""` is honored verbatim (only omitting the prop falls back to the computed default).
|
|
47
|
+
|
|
48
|
+
## Styling — Tailwind setup for consumers
|
|
49
|
+
|
|
50
|
+
This package ships **no precompiled CSS** — the component utility classes are inlined in the `dist/` JavaScript, and Tailwind v4 does not scan `node_modules`. Without help, those utilities never make it into your build, so the components render unstyled.
|
|
51
|
+
|
|
52
|
+
The fix is to import the package's build-generated safelist into your Tailwind CSS entry, right next to your `@import "tailwindcss";`:
|
|
53
|
+
|
|
54
|
+
```css
|
|
55
|
+
@import "tailwindcss";
|
|
56
|
+
@import "@takazudo/zudo-doc/safelist.css";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`dist/safelist.css` is generated at package build time and contains an `@source inline()` set covering every utility the components use (including arbitrary-value classes like `w-[var(--zd-sidebar-w)]`). It auto-syncs whenever you upgrade the package — no drift, no manual maintenance. Available in `@takazudo/zudo-doc` **>= 0.2.0**.
|
|
60
|
+
|
|
61
|
+
> **Don't `@source` into `node_modules`.** A glob like `@source "../node_modules/@takazudo/zudo-doc/dist/**"` looks plausible but is unreliable: pnpm surfaces packages via symlinks and Tailwind v4's file scanner does not reliably traverse them, so utilities get intermittently dropped across rebuilds (see zudolab/zudo-doc#1989). Import the package safelist instead.
|
|
62
|
+
|
|
63
|
+
**Migrating from a pre-0.2.0 workaround?** If you vendored or copied the package `dist/` to get its styles, delete that workaround and replace it with the single `@import "@takazudo/zudo-doc/safelist.css";` line above.
|
|
64
|
+
|
|
65
|
+
## Dev workflow (in this repo)
|
|
66
|
+
|
|
67
|
+
This package is published to npm as `@takazudo/zudo-doc` (since `0.2.0`, `latest` tracks the current line). Inside this repo the host site consumes it as a workspace package through its compiled `dist/` — `pnpm dev` at the repo root runs `tsup --watch` so edits under `src/` rebuild automatically; for a one-off rebuild use `pnpm --filter @takazudo/zudo-doc build`.
|
|
68
|
+
|
|
69
|
+
zfb itself comes from npm (versions pinned in the root `package.json`). To develop against a local zfb checkout, use the temporary `pnpm.overrides` link escape hatch documented in the root `CLAUDE.md` — do not commit the override.
|
|
@@ -59,25 +59,31 @@ function DocLayout(props) {
|
|
|
59
59
|
}
|
|
60
60
|
),
|
|
61
61
|
afterSidebar,
|
|
62
|
-
/* @__PURE__ */ jsxs(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
children:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
/* @__PURE__ */ jsxs(
|
|
63
|
+
"div",
|
|
64
|
+
{
|
|
65
|
+
class: `zd-sidebar-content-wrapper${showSidebar ? " lg:ml-[var(--zd-sidebar-w)]" : ""}`,
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ jsx("div", { class: "flex min-h-[calc(100vh-3.5rem)] justify-center", children: /* @__PURE__ */ jsxs(
|
|
68
|
+
"div",
|
|
69
|
+
{
|
|
70
|
+
class: `zd-doc-content-band flex w-full gap-[clamp(1.5rem,3vw,4rem)] ${showSidebar ? "max-w-[clamp(50rem,75vw,90rem)]" : "max-w-[80rem]"}`,
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("main", { class: "flex-1 min-w-0 px-hsp-xl py-vsp-xl lg:px-hsp-2xl lg:py-vsp-2xl", children: [
|
|
73
|
+
breadcrumb,
|
|
74
|
+
afterBreadcrumb,
|
|
75
|
+
!hideToc && mobileToc,
|
|
76
|
+
/* @__PURE__ */ jsx("article", { class: "zd-content max-w-none", children: main }),
|
|
77
|
+
afterContent
|
|
78
|
+
] }),
|
|
79
|
+
showToc && toc
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
) }),
|
|
83
|
+
footer
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
),
|
|
81
87
|
bodyEndComponents,
|
|
82
88
|
bodyEndScripts
|
|
83
89
|
] })
|
|
@@ -17,17 +17,6 @@ interface NavItemLike {
|
|
|
17
17
|
* pathForMatch(null, "ja", "en") === ""
|
|
18
18
|
*/
|
|
19
19
|
declare function pathForMatch(pathWithoutBase: string, lang: string | undefined, defaultLocale: string): string;
|
|
20
|
-
/**
|
|
21
|
-
* Pick the deepest header-nav path that the current page lives under.
|
|
22
|
-
*
|
|
23
|
-
* The legacy template flattens parent + child paths, filters by
|
|
24
|
-
* `pathForMatch.startsWith(p)`, and picks the longest match — this keeps
|
|
25
|
-
* a child link active even when its parent's path is also a prefix of
|
|
26
|
-
* the current page.
|
|
27
|
-
*
|
|
28
|
-
* Returns `undefined` when no nav entry matches; callers should treat
|
|
29
|
-
* that as "no active link."
|
|
30
|
-
*/
|
|
31
20
|
declare function computeActiveNavPath(navItems: readonly NavItemLike[], pathForMatchValue: string): string | undefined;
|
|
32
21
|
/**
|
|
33
22
|
* Active-state predicate for a top-level nav item: the item itself, or
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
function pathForMatch(pathWithoutBase, lang, defaultLocale) {
|
|
2
2
|
if (lang == null || lang === defaultLocale) return pathWithoutBase;
|
|
3
|
-
return pathWithoutBase.replace(new RegExp(`^/${lang}`), "");
|
|
3
|
+
return pathWithoutBase.replace(new RegExp(`^/${lang}(?=/|$)`), "");
|
|
4
|
+
}
|
|
5
|
+
function pathMatchesNavPath(currentPath, navPath) {
|
|
6
|
+
if (currentPath === navPath) return true;
|
|
7
|
+
const prefix = navPath.endsWith("/") ? navPath : `${navPath}/`;
|
|
8
|
+
return currentPath.startsWith(prefix);
|
|
4
9
|
}
|
|
5
10
|
function computeActiveNavPath(navItems, pathForMatchValue) {
|
|
6
11
|
const allNavPaths = navItems.flatMap((item) => {
|
|
@@ -10,7 +15,7 @@ function computeActiveNavPath(navItems, pathForMatchValue) {
|
|
|
10
15
|
}
|
|
11
16
|
return paths;
|
|
12
17
|
});
|
|
13
|
-
return allNavPaths.filter((p) => pathForMatchValue
|
|
18
|
+
return allNavPaths.filter((p) => pathMatchesNavPath(pathForMatchValue, p)).sort((a, b) => b.length - a.length)[0];
|
|
14
19
|
}
|
|
15
20
|
function isNavItemActive(item, activeNavPath) {
|
|
16
21
|
if (activeNavPath === item.path) return true;
|
|
@@ -42,6 +42,19 @@ interface HtmlPreviewWrapperProps {
|
|
|
42
42
|
height?: number;
|
|
43
43
|
/** When true, the code section is expanded by default. */
|
|
44
44
|
defaultOpen?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* iframe `sandbox` attribute value, forwarded to `<HtmlPreview>`. Omit to
|
|
47
|
+
* use the computed default (`allow-scripts allow-same-origin` when scripts
|
|
48
|
+
* are present, `allow-same-origin` otherwise).
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ The default voids iframe sandboxing (preview scripts can reach the
|
|
51
|
+
* parent origin) and is only safe for **author-trusted** content.
|
|
52
|
+
* Downstream consumers rendering semi-trusted HTML should pass a stricter
|
|
53
|
+
* value (e.g. `"allow-scripts"` or `""`) — but note that dropping
|
|
54
|
+
* `allow-same-origin` disables auto-height, so pair it with a fixed
|
|
55
|
+
* `height`. See `HtmlPreviewProps.sandbox` for details.
|
|
56
|
+
*/
|
|
57
|
+
sandbox?: string;
|
|
45
58
|
}
|
|
46
59
|
/**
|
|
47
60
|
* Bare HTML preview body — the actual island **hydration target**.
|
|
@@ -3,7 +3,17 @@ import { jsx } from "preact/jsx-runtime";
|
|
|
3
3
|
import { Island } from "@takazudo/zfb";
|
|
4
4
|
import { HtmlPreview } from "./html-preview.js";
|
|
5
5
|
function HtmlPreviewWrapperInner(props) {
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
globalConfig,
|
|
8
|
+
html,
|
|
9
|
+
css,
|
|
10
|
+
head,
|
|
11
|
+
js,
|
|
12
|
+
title,
|
|
13
|
+
height,
|
|
14
|
+
defaultOpen,
|
|
15
|
+
sandbox
|
|
16
|
+
} = props;
|
|
7
17
|
const mergedHead = [globalConfig?.head, head].filter(Boolean).join("\n") || void 0;
|
|
8
18
|
const mergedCss = [globalConfig?.css, css].filter(Boolean).join("\n") || void 0;
|
|
9
19
|
const mergedJs = [globalConfig?.js, js].filter(Boolean).join("\n") || void 0;
|
|
@@ -17,6 +27,7 @@ function HtmlPreviewWrapperInner(props) {
|
|
|
17
27
|
title,
|
|
18
28
|
height,
|
|
19
29
|
defaultOpen,
|
|
30
|
+
sandbox,
|
|
20
31
|
componentCss: css,
|
|
21
32
|
componentHead: head,
|
|
22
33
|
componentJs: js
|
|
@@ -11,6 +11,27 @@ interface HtmlPreviewProps {
|
|
|
11
11
|
title?: string;
|
|
12
12
|
height?: number;
|
|
13
13
|
defaultOpen?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* iframe `sandbox` attribute value. When omitted, defaults to the value
|
|
16
|
+
* computed from the preview content (`allow-scripts allow-same-origin`
|
|
17
|
+
* when scripts are present, `allow-same-origin` otherwise — see
|
|
18
|
+
* {@link resolveSandbox}).
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ **Trust assumption.** The default keeps `allow-same-origin`, which
|
|
21
|
+
* combined with `allow-scripts` effectively *voids* the iframe sandbox:
|
|
22
|
+
* scripts inside the preview can reach the parent document's origin. This
|
|
23
|
+
* is safe only for **author-trusted** content (e.g. zudo-doc's own
|
|
24
|
+
* MDX-authored previews). Downstream consumers rendering semi-trusted or
|
|
25
|
+
* user-submitted HTML should pass a stricter value (e.g. `"allow-scripts"`
|
|
26
|
+
* or `""`).
|
|
27
|
+
*
|
|
28
|
+
* Note: removing `allow-same-origin` breaks the auto-height mechanism
|
|
29
|
+
* (the parent can no longer read `iframe.contentDocument` to measure the
|
|
30
|
+
* body), so set a fixed {@link HtmlPreviewProps.height} when you do.
|
|
31
|
+
* Passing the empty string `""` (maximally restrictive) is honored —
|
|
32
|
+
* only `undefined` triggers the computed default.
|
|
33
|
+
*/
|
|
34
|
+
sandbox?: string;
|
|
14
35
|
/** Per-component css for code block display (before global merge) */
|
|
15
36
|
componentCss?: string;
|
|
16
37
|
/** Per-component head for code block display (before global merge) */
|
|
@@ -18,6 +39,26 @@ interface HtmlPreviewProps {
|
|
|
18
39
|
/** Per-component js for code block display (before global merge) */
|
|
19
40
|
componentJs?: string;
|
|
20
41
|
}
|
|
42
|
+
declare function containsScript(head?: string, js?: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the iframe `sandbox` attribute value.
|
|
45
|
+
*
|
|
46
|
+
* An explicit `sandbox` prop wins (including the empty string `""`, the most
|
|
47
|
+
* restrictive value — only `undefined` falls through). Otherwise the default
|
|
48
|
+
* is derived from `hasScripts`:
|
|
49
|
+
*
|
|
50
|
+
* - with scripts → `"allow-scripts allow-same-origin"`
|
|
51
|
+
* - without scripts → `"allow-same-origin"`
|
|
52
|
+
*
|
|
53
|
+
* `allow-same-origin` is kept in BOTH defaults on purpose: the parent reads
|
|
54
|
+
* `iframe.contentDocument` for auto-height measurement, and a srcdoc iframe
|
|
55
|
+
* sandboxed without `allow-same-origin` gets an opaque origin that blocks
|
|
56
|
+
* those reads even from the parent page. The combination voids sandboxing
|
|
57
|
+
* for script-bearing previews — acceptable here because preview content is
|
|
58
|
+
* author-trusted MDX. See {@link HtmlPreviewProps.sandbox} for the downstream
|
|
59
|
+
* trust note.
|
|
60
|
+
*/
|
|
61
|
+
declare function resolveSandbox(sandbox: string | undefined, hasScripts: boolean): string;
|
|
21
62
|
/**
|
|
22
63
|
* HTML preview widget — renders an isolated iframe with viewport
|
|
23
64
|
* controls and a collapsible code section.
|
|
@@ -30,6 +71,6 @@ interface HtmlPreviewProps {
|
|
|
30
71
|
* Astro, or wire up the SSR-skip placeholder pattern for non-Astro
|
|
31
72
|
* consumers.
|
|
32
73
|
*/
|
|
33
|
-
declare function HtmlPreview({ html, css, head, js, title, height, defaultOpen, componentCss, componentHead, componentJs, }: HtmlPreviewProps): VNode;
|
|
74
|
+
declare function HtmlPreview({ html, css, head, js, title, height, defaultOpen, sandbox, componentCss, componentHead, componentJs, }: HtmlPreviewProps): VNode;
|
|
34
75
|
|
|
35
|
-
export { HtmlPreview, type HtmlPreviewProps };
|
|
76
|
+
export { HtmlPreview, type HtmlPreviewProps, containsScript, resolveSandbox };
|
|
@@ -8,6 +8,9 @@ function containsScript(head, js) {
|
|
|
8
8
|
if (head && /<script/i.test(head)) return true;
|
|
9
9
|
return false;
|
|
10
10
|
}
|
|
11
|
+
function resolveSandbox(sandbox, hasScripts) {
|
|
12
|
+
return sandbox ?? (hasScripts ? "allow-scripts allow-same-origin" : "allow-same-origin");
|
|
13
|
+
}
|
|
11
14
|
function buildSrcdoc(html, css, head, js) {
|
|
12
15
|
return `<!doctype html>
|
|
13
16
|
<html>
|
|
@@ -31,6 +34,7 @@ function HtmlPreview({
|
|
|
31
34
|
title,
|
|
32
35
|
height,
|
|
33
36
|
defaultOpen,
|
|
37
|
+
sandbox,
|
|
34
38
|
componentCss,
|
|
35
39
|
componentHead,
|
|
36
40
|
componentJs
|
|
@@ -41,7 +45,7 @@ function HtmlPreview({
|
|
|
41
45
|
);
|
|
42
46
|
const hasScripts = containsScript(head, js);
|
|
43
47
|
const syncDelay = hasScripts ? 300 : 0;
|
|
44
|
-
const sandboxValue = hasScripts
|
|
48
|
+
const sandboxValue = resolveSandbox(sandbox, hasScripts);
|
|
45
49
|
const codeBlocks = useMemo(
|
|
46
50
|
() => [
|
|
47
51
|
{ language: "html", title: "HTML", code: dedent(html) },
|
|
@@ -71,5 +75,7 @@ function HtmlPreview({
|
|
|
71
75
|
);
|
|
72
76
|
}
|
|
73
77
|
export {
|
|
74
|
-
HtmlPreview
|
|
78
|
+
HtmlPreview,
|
|
79
|
+
containsScript,
|
|
80
|
+
resolveSandbox
|
|
75
81
|
};
|
|
@@ -19,7 +19,7 @@ function parseFrontmatter(content) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
function escapeTitle(s) {
|
|
22
|
-
return s.replace(/"/g, '\\"');
|
|
22
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
23
23
|
}
|
|
24
24
|
function listFiles(dir) {
|
|
25
25
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -36,6 +36,25 @@ generated: true
|
|
|
36
36
|
`;
|
|
37
37
|
fs.writeFileSync(path.join(outputDir, "index.mdx"), mdx);
|
|
38
38
|
}
|
|
39
|
+
function writeUnlistedSubPage(outputPath, title, slug, body) {
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
outputPath,
|
|
42
|
+
`---
|
|
43
|
+
title: "${escapeTitle(title)}"
|
|
44
|
+
slug: "${slug}"
|
|
45
|
+
unlisted: true
|
|
46
|
+
generated: true
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
${body}
|
|
50
|
+
`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function assertNotIndexReserved(nameOrSlug, errorMessage) {
|
|
54
|
+
if (nameOrSlug === "index") {
|
|
55
|
+
throw new Error(errorMessage);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
function findClaudeMdFiles(dir, excludeDirs) {
|
|
40
59
|
const results = [];
|
|
41
60
|
if (!fs.existsSync(dir)) return results;
|
|
@@ -92,11 +111,10 @@ function generateClaudemdDocs(config) {
|
|
|
92
111
|
});
|
|
93
112
|
const emittedSlugs = /* @__PURE__ */ new Map();
|
|
94
113
|
items.forEach((item, index) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
114
|
+
assertNotIndexReserved(
|
|
115
|
+
item.slug,
|
|
116
|
+
`claude-resources: "${item.relPath}" maps to the reserved slug "index", which is used for the category metadata file. Rename the directory to resolve the conflict.`
|
|
117
|
+
);
|
|
100
118
|
const previous = emittedSlugs.get(item.slug);
|
|
101
119
|
if (previous !== void 0) {
|
|
102
120
|
throw new Error(
|
|
@@ -136,11 +154,10 @@ function generateCommandsDocs(config) {
|
|
|
136
154
|
const parsed = parseFrontmatter(content);
|
|
137
155
|
if (!parsed) continue;
|
|
138
156
|
const name = file.replace(/\.md$/, "");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
157
|
+
assertNotIndexReserved(
|
|
158
|
+
name,
|
|
159
|
+
`claude-resources: ".claude/commands/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the command file to resolve the conflict.`
|
|
160
|
+
);
|
|
144
161
|
const description = parsed.data.description || "";
|
|
145
162
|
items.push({ name, description });
|
|
146
163
|
const mdx = `---
|
|
@@ -220,11 +237,10 @@ function generateSkillsDocs(config) {
|
|
|
220
237
|
ensureDir(outputDir);
|
|
221
238
|
const items = [];
|
|
222
239
|
for (const dir of dirs) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
240
|
+
assertNotIndexReserved(
|
|
241
|
+
dir,
|
|
242
|
+
`claude-resources: skill directory ".claude/skills/index/" uses the reserved name "index", which is used for the category metadata file. Rename the skill directory to resolve the conflict.`
|
|
243
|
+
);
|
|
228
244
|
const content = fs.readFileSync(
|
|
229
245
|
path.join(skillsDir, dir, "SKILL.md"),
|
|
230
246
|
"utf8"
|
|
@@ -284,60 +300,41 @@ ${body}`;
|
|
|
284
300
|
fs.writeFileSync(path.join(outputDir, `${dir}.mdx`), mdx);
|
|
285
301
|
const skillSlugBase = `claude-skills/${dir}`;
|
|
286
302
|
for (const ref of references) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
${escapeForMdx(ref.content.trim())}
|
|
296
|
-
`;
|
|
297
|
-
fs.writeFileSync(path.join(outputDir, `${dir}--ref-${ref.name}.mdx`), refMdx);
|
|
303
|
+
writeUnlistedSubPage(
|
|
304
|
+
path.join(outputDir, `${dir}--ref-${ref.name}.mdx`),
|
|
305
|
+
ref.title,
|
|
306
|
+
`${skillSlugBase}/ref-${ref.name}`,
|
|
307
|
+
escapeForMdx(ref.content.trim())
|
|
308
|
+
);
|
|
298
309
|
}
|
|
299
310
|
for (const f of scriptFiles.filter((s) => s.endsWith(".md"))) {
|
|
300
311
|
const slug = f.replace(/\.md$/, "");
|
|
301
|
-
const subSlug = `${skillSlugBase}/script-${slug}`;
|
|
302
312
|
const raw = fs.readFileSync(
|
|
303
313
|
path.join(skillsDir, dir, "scripts", f),
|
|
304
314
|
"utf8"
|
|
305
315
|
);
|
|
306
316
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
307
317
|
const title = h1Match?.[1] ?? slug;
|
|
308
|
-
|
|
318
|
+
writeUnlistedSubPage(
|
|
309
319
|
path.join(outputDir, `${dir}--script-${slug}.mdx`),
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
unlisted: true
|
|
314
|
-
generated: true
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
${escapeForMdx(raw.trim())}
|
|
318
|
-
`
|
|
320
|
+
title,
|
|
321
|
+
`${skillSlugBase}/script-${slug}`,
|
|
322
|
+
escapeForMdx(raw.trim())
|
|
319
323
|
);
|
|
320
324
|
}
|
|
321
325
|
for (const f of assetFiles.filter((a) => a.endsWith(".md"))) {
|
|
322
326
|
const slug = f.replace(/\.md$/, "");
|
|
323
|
-
const subSlug = `${skillSlugBase}/asset-${slug}`;
|
|
324
327
|
const raw = fs.readFileSync(
|
|
325
328
|
path.join(skillsDir, dir, "assets", f),
|
|
326
329
|
"utf8"
|
|
327
330
|
);
|
|
328
331
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
329
332
|
const title = h1Match?.[1] ?? slug;
|
|
330
|
-
|
|
333
|
+
writeUnlistedSubPage(
|
|
331
334
|
path.join(outputDir, `${dir}--asset-${slug}.mdx`),
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
unlisted: true
|
|
336
|
-
generated: true
|
|
337
|
-
---
|
|
338
|
-
|
|
339
|
-
${escapeForMdx(raw.trim())}
|
|
340
|
-
`
|
|
335
|
+
title,
|
|
336
|
+
`${skillSlugBase}/asset-${slug}`,
|
|
337
|
+
escapeForMdx(raw.trim())
|
|
341
338
|
);
|
|
342
339
|
}
|
|
343
340
|
}
|
|
@@ -362,11 +359,10 @@ function generateAgentsDocs(config) {
|
|
|
362
359
|
const description = parsed.data.description || "";
|
|
363
360
|
const model = parsed.data.model || "";
|
|
364
361
|
const fileSlug = file.replace(/\.md$/, "");
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
362
|
+
assertNotIndexReserved(
|
|
363
|
+
fileSlug,
|
|
364
|
+
`claude-resources: ".claude/agents/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the agent file to resolve the conflict.`
|
|
365
|
+
);
|
|
370
366
|
items.push({ name, file: fileSlug, description, model });
|
|
371
367
|
const modelBadge = model ? `**Model:** \`${model}\`
|
|
372
368
|
` : "";
|
|
@@ -39,15 +39,23 @@ interface DocHistoryMetaEntry {
|
|
|
39
39
|
author: string;
|
|
40
40
|
createdDate: string;
|
|
41
41
|
updatedDate: string;
|
|
42
|
+
/**
|
|
43
|
+
* Source file extension (".mdx" or ".md") — the content walkers accept
|
|
44
|
+
* both (`collectContentFiles` matches `\.mdx?$`), so the view-source URL
|
|
45
|
+
* builder in the host's `_doc-history-area.tsx` reads this instead of
|
|
46
|
+
* hardcoding ".mdx". Optional in older manifests; readers fall back to
|
|
47
|
+
* ".mdx" when absent.
|
|
48
|
+
*/
|
|
49
|
+
ext?: ".mdx" | ".md";
|
|
42
50
|
}
|
|
43
51
|
/** Manifest shape — keyed by composedSlug. */
|
|
44
52
|
type DocHistoryMetaManifest = Record<string, DocHistoryMetaEntry>;
|
|
45
53
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
54
|
+
* Derive the manifest `ext` value from a content file path. The walkers
|
|
55
|
+
* only collect `.md` / `.mdx` files (`collectContentFiles` matches
|
|
56
|
+
* `\.mdx?$`), so anything not ending in ".md" is ".mdx".
|
|
49
57
|
*/
|
|
50
|
-
declare function
|
|
58
|
+
declare function deriveSourceExt(filePath: string): ".mdx" | ".md";
|
|
51
59
|
/**
|
|
52
60
|
* Emit `<projectRoot>/.zfb/doc-history-meta.json` from git history.
|
|
53
61
|
*
|
|
@@ -61,4 +69,4 @@ declare function makeSemaphore(concurrency: number): () => Promise<() => void>;
|
|
|
61
69
|
*/
|
|
62
70
|
declare function runDocHistoryMetaStep(options: RunDocHistoryMetaOptions): Promise<void>;
|
|
63
71
|
|
|
64
|
-
export { type DocHistoryMetaEntry, type DocHistoryMetaLocaleConfig, type DocHistoryMetaManifest, type DocHistoryMetaVersionConfig, type RunDocHistoryMetaOptions,
|
|
72
|
+
export { type DocHistoryMetaEntry, type DocHistoryMetaLocaleConfig, type DocHistoryMetaManifest, type DocHistoryMetaVersionConfig, type RunDocHistoryMetaOptions, deriveSourceExt, runDocHistoryMetaStep };
|
|
@@ -1,36 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { cpus } from "node:os";
|
|
4
3
|
import {
|
|
5
4
|
collectContentFiles,
|
|
6
5
|
getFileCommitsMetaAsync
|
|
7
6
|
} from "@takazudo/zudo-doc-history-server/git-history";
|
|
7
|
+
import {
|
|
8
|
+
makeSemaphore,
|
|
9
|
+
defaultGitConcurrency
|
|
10
|
+
} from "@takazudo/zudo-doc-history-server/concurrency";
|
|
8
11
|
const META_OUT_RELATIVE_DIR = ".zfb";
|
|
9
12
|
const META_OUT_FILENAME = "doc-history-meta.json";
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
const queue = [];
|
|
13
|
-
function next() {
|
|
14
|
-
if (queue.length > 0 && running < concurrency) {
|
|
15
|
-
queue.shift()();
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return function acquire() {
|
|
19
|
-
return new Promise((resolve) => {
|
|
20
|
-
function tryRun() {
|
|
21
|
-
if (running < concurrency) {
|
|
22
|
-
running++;
|
|
23
|
-
resolve(() => {
|
|
24
|
-
running--;
|
|
25
|
-
next();
|
|
26
|
-
});
|
|
27
|
-
} else {
|
|
28
|
-
queue.push(tryRun);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
tryRun();
|
|
32
|
-
});
|
|
33
|
-
};
|
|
13
|
+
function deriveSourceExt(filePath) {
|
|
14
|
+
return filePath.endsWith(".md") ? ".md" : ".mdx";
|
|
34
15
|
}
|
|
35
16
|
async function runDocHistoryMetaStep(options) {
|
|
36
17
|
const projectRoot = path.resolve(options.projectRoot);
|
|
@@ -53,7 +34,7 @@ async function runDocHistoryMetaStep(options) {
|
|
|
53
34
|
dirEntries.push([code, path.resolve(projectRoot, locale.dir)]);
|
|
54
35
|
}
|
|
55
36
|
}
|
|
56
|
-
const concurrency =
|
|
37
|
+
const concurrency = defaultGitConcurrency();
|
|
57
38
|
const acquire = makeSemaphore(concurrency);
|
|
58
39
|
const jobs = [];
|
|
59
40
|
for (const [localeKey, contentDir] of dirEntries) {
|
|
@@ -90,7 +71,9 @@ async function runDocHistoryMetaStep(options) {
|
|
|
90
71
|
author: result.oldest.author,
|
|
91
72
|
// createdDate = oldest commit; updatedDate = newest commit.
|
|
92
73
|
createdDate: result.oldest.date,
|
|
93
|
-
updatedDate: result.newest.date
|
|
74
|
+
updatedDate: result.newest.date,
|
|
75
|
+
// Source extension for the view-source URL (".md" walkers accepted).
|
|
76
|
+
ext: deriveSourceExt(jobs[i].filePath)
|
|
94
77
|
};
|
|
95
78
|
}
|
|
96
79
|
fs.mkdirSync(zfbDir, { recursive: true });
|
|
@@ -108,6 +91,6 @@ const defaultLogger = {
|
|
|
108
91
|
}
|
|
109
92
|
};
|
|
110
93
|
export {
|
|
111
|
-
|
|
94
|
+
deriveSourceExt,
|
|
112
95
|
runDocHistoryMetaStep
|
|
113
96
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { LlmsTxtSiteMeta, LlmsTxtLocaleConfig } from './types.js';
|
|
3
|
+
import '../../md-utils/index.js';
|
|
3
4
|
|
|
4
5
|
/** Connect-style middleware signature — works as a Vite plugin middleware. */
|
|
5
6
|
type LlmsTxtNextFn = (err?: unknown) => void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { LlmsTxtDevMiddlewareOptions, LlmsTxtMiddleware, LlmsTxtMiddlewareLogger, LlmsTxtNextFn, createLlmsTxtDevMiddleware } from './dev-middleware.js';
|
|
2
2
|
export { emitLlmsTxt } from './emit.js';
|
|
3
3
|
export { generateLlmsFullTxt, generateLlmsTxt } from './generate.js';
|
|
4
|
-
export {
|
|
5
|
-
export { stripImportsAndJsx
|
|
4
|
+
export { loadDocEntries } from './load.js';
|
|
5
|
+
export { stripImportsAndJsx } from './strip.js';
|
|
6
6
|
export { LlmsDocEntry, LlmsTxtEmitOptions, LlmsTxtEmitResult, LlmsTxtFrontmatter, LlmsTxtLoadOptions, LlmsTxtLocaleConfig, LlmsTxtLogger, LlmsTxtSiteMeta } from './types.js';
|
|
7
|
+
export { collectMdFiles, isExcluded, parseMarkdownFile, slugToUrl, stripMarkdown } from '../../md-utils/index.js';
|
|
7
8
|
import 'node:http';
|