@kpritam/grimoire-output-docusaurus 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/assets.d.ts +9 -0
- package/dist/internal/assets.js +50 -0
- package/dist/internal/docusaurusConfig.d.ts +9 -0
- package/dist/internal/docusaurusConfig.js +259 -0
- package/dist/internal/spellbookAssets.d.ts +39 -0
- package/dist/internal/spellbookAssets.js +68 -0
- package/dist/layer.d.ts +3 -0
- package/dist/layer.js +6 -0
- package/dist/shared.d.ts +10 -0
- package/dist/shared.js +36 -0
- package/dist/upstream.d.ts +6 -0
- package/dist/upstream.js +84 -0
- package/package.json +59 -0
- package/src/index.ts +1 -0
- package/src/internal/assets.ts +66 -0
- package/src/internal/docusaurusConfig.ts +281 -0
- package/src/internal/spellbookAssets.ts +80 -0
- package/src/layer.ts +12 -0
- package/src/shared.ts +43 -0
- package/src/upstream.ts +119 -0
- package/templates/spellbook/spellbookPlugin.ts +156 -0
- package/templates/spellbook/src/components/SpellbookChat/ChatEngine.ts +79 -0
- package/templates/spellbook/src/components/SpellbookChat/ChatErrorBoundary.tsx +65 -0
- package/templates/spellbook/src/components/SpellbookChat/Markdown.tsx +259 -0
- package/templates/spellbook/src/components/SpellbookChat/README.md +111 -0
- package/templates/spellbook/src/components/SpellbookChat/SettingsPanel.tsx +376 -0
- package/templates/spellbook/src/components/SpellbookChat/VoiceMode.tsx +867 -0
- package/templates/spellbook/src/components/SpellbookChat/index.tsx +744 -0
- package/templates/spellbook/src/components/SpellbookChat/markdown.module.css +343 -0
- package/templates/spellbook/src/components/SpellbookChat/secretStore.ts +106 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/anthropic.ts +36 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/createCloudProvider.ts +112 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/google.ts +33 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/index.ts +32 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/mapFinishReason.ts +23 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/ollama.ts +44 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/openai.ts +34 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/openaiRealtime.ts +320 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/types.ts +172 -0
- package/templates/spellbook/src/components/SpellbookChat/streamProviders/webllm.ts +214 -0
- package/templates/spellbook/src/components/SpellbookChat/styles.module.css +852 -0
- package/templates/spellbook/src/components/SpellbookChat/systemPrompt.ts +107 -0
- package/templates/spellbook/src/components/SpellbookChat/transformers-ssr-stub.ts +16 -0
- package/templates/spellbook/src/components/SpellbookChat/types.ts +52 -0
- package/templates/spellbook/src/components/SpellbookChat/useBundleLoader.ts +46 -0
- package/templates/spellbook/src/components/SpellbookChat/useChatEngine.ts +524 -0
- package/templates/spellbook/src/components/SpellbookChat/useEmbeddings.ts +147 -0
- package/templates/spellbook/src/components/SpellbookChat/useRetrieval.ts +377 -0
- package/templates/spellbook/src/components/SpellbookChat/useSileroVAD.ts +236 -0
- package/templates/spellbook/src/components/SpellbookChat/useSpeechRecognition.ts +271 -0
- package/templates/spellbook/src/components/SpellbookChat/useSpeechSynthesis.ts +229 -0
- package/templates/spellbook/src/components/SpellbookChat/useUnifiedSTT.ts +134 -0
- package/templates/spellbook/src/components/SpellbookChat/useWhisperSTT.ts +411 -0
- package/templates/spellbook/src/components/SpellbookChat/vad-ssr-stub.ts +25 -0
- package/templates/spellbook/src/components/SpellbookChat/voiceDebug.ts +60 -0
- package/templates/spellbook/src/components/SpellbookChat/voiceFsm.ts +196 -0
- package/templates/spellbook/src/components/SpellbookChat/voiceStyles.module.css +334 -0
- package/templates/spellbook/src/components/SpellbookChat/webllm-ssr-stub.ts +8 -0
- package/templates/spellbook/src/components/SpellbookChatDisabled.tsx +20 -0
- package/templates/spellbook/src/theme/Root.tsx +29 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
const genImports = (spellbook: boolean): string => {
|
|
2
|
+
const spellbookImport = spellbook
|
|
3
|
+
? `import { spellbookWebpackPlugin } from "./spellbookPlugin";\n`
|
|
4
|
+
: ""
|
|
5
|
+
return `import type { Config } from "@docusaurus/types";
|
|
6
|
+
import type * as Preset from "@docusaurus/preset-classic";
|
|
7
|
+
import { themes as prismThemes } from "prism-react-renderer";
|
|
8
|
+
${spellbookImport}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const genWatcherEnv = (): string => `if (!Object.hasOwn(process.env, "WATCHPACK_POLLING")) {
|
|
12
|
+
process.env.WATCHPACK_POLLING = "true";
|
|
13
|
+
}
|
|
14
|
+
if (!Object.hasOwn(process.env, "CHOKIDAR_USEPOLLING")) {
|
|
15
|
+
process.env.CHOKIDAR_USEPOLLING = "true";
|
|
16
|
+
}
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
const genDevWatchPlugin = (): string => `const grimoireDevWatchPlugin = () => ({
|
|
20
|
+
name: "grimoire-dev-watch",
|
|
21
|
+
configureWebpack() {
|
|
22
|
+
return {
|
|
23
|
+
watchOptions: {
|
|
24
|
+
ignored: [
|
|
25
|
+
"**/.git/**",
|
|
26
|
+
"**/node_modules/**",
|
|
27
|
+
"**/.docusaurus/**",
|
|
28
|
+
"**/build/**",
|
|
29
|
+
"**/dist/**",
|
|
30
|
+
"**/.turbo/**",
|
|
31
|
+
"**/.next/**",
|
|
32
|
+
"**/coverage/**",
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
const genSidebarHelpers = (): string => `const SIDEBAR_ACRONYMS = new Set([
|
|
41
|
+
"cli", "ci", "cd", "ai", "api", "sdk", "url", "uri", "http", "https",
|
|
42
|
+
"json", "yaml", "md", "mdx", "sha", "dsl", "ide", "ui", "ux", "io",
|
|
43
|
+
"pr", "mr", "os", "sql", "tls", "ssh", "cors", "dns", "tcp", "udp", "jwt",
|
|
44
|
+
]);
|
|
45
|
+
const humanizeSidebarLabel = (slug: string): string =>
|
|
46
|
+
slug
|
|
47
|
+
.split(/[-_\\s]+/)
|
|
48
|
+
.map((w, i) => {
|
|
49
|
+
if (!w) return w;
|
|
50
|
+
if (SIDEBAR_ACRONYMS.has(w.toLowerCase())) return w.toUpperCase();
|
|
51
|
+
if (/^v\\d/.test(w)) return w.toLowerCase();
|
|
52
|
+
return i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w;
|
|
53
|
+
})
|
|
54
|
+
.join(" ");
|
|
55
|
+
|
|
56
|
+
const SIDEBAR_WEIGHTS: Record<string, number> = {
|
|
57
|
+
"index": -1000,
|
|
58
|
+
"introduction": -900,
|
|
59
|
+
"intro": -900,
|
|
60
|
+
"getting-started": -800,
|
|
61
|
+
"quickstart": -780,
|
|
62
|
+
"quick-start": -780,
|
|
63
|
+
"installation": -760,
|
|
64
|
+
"install": -760,
|
|
65
|
+
"tutorial": -700,
|
|
66
|
+
"overview": -680,
|
|
67
|
+
"concepts": -400,
|
|
68
|
+
"guides": -300,
|
|
69
|
+
"how-to": -280,
|
|
70
|
+
"recipes": -260,
|
|
71
|
+
"reference": 200,
|
|
72
|
+
"api": 220,
|
|
73
|
+
"cli": 220,
|
|
74
|
+
"configuration": 230,
|
|
75
|
+
"config": 230,
|
|
76
|
+
"architecture": 600,
|
|
77
|
+
"internals": 700,
|
|
78
|
+
"development": 780,
|
|
79
|
+
"contributing": 800,
|
|
80
|
+
"roadmap": 880,
|
|
81
|
+
"changelog": 900,
|
|
82
|
+
"faq": 920,
|
|
83
|
+
"troubleshooting": 940,
|
|
84
|
+
};
|
|
85
|
+
const sidebarWeight = (slug: string): number =>
|
|
86
|
+
SIDEBAR_WEIGHTS[slug.toLowerCase()] ?? 0;
|
|
87
|
+
`
|
|
88
|
+
|
|
89
|
+
const genSidebarItemsGenerator = (): string => ` sidebarItemsGenerator: async ({
|
|
90
|
+
defaultSidebarItemsGenerator,
|
|
91
|
+
...args
|
|
92
|
+
}) => {
|
|
93
|
+
const items = await defaultSidebarItemsGenerator(args);
|
|
94
|
+
const hiddenAnywhere = new Set(["README"]);
|
|
95
|
+
const hiddenAtRoot = new Set(["index"]);
|
|
96
|
+
const slugTail = (id: string): string => {
|
|
97
|
+
const segs = id.split("/");
|
|
98
|
+
return segs[segs.length - 1] ?? id;
|
|
99
|
+
};
|
|
100
|
+
const looksRawSlug = (s: string): boolean =>
|
|
101
|
+
/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(s);
|
|
102
|
+
type Item = (typeof items)[number];
|
|
103
|
+
const itemKey = (item: Item): string => {
|
|
104
|
+
if (item.type === "doc") return slugTail(item.id).toLowerCase();
|
|
105
|
+
if (item.type === "category") return item.label.toLowerCase();
|
|
106
|
+
return "";
|
|
107
|
+
};
|
|
108
|
+
const sortTopLevel = (list: Item[]): Item[] =>
|
|
109
|
+
[...list].sort((a, b) => {
|
|
110
|
+
const ka = itemKey(a);
|
|
111
|
+
const kb = itemKey(b);
|
|
112
|
+
const wa = sidebarWeight(ka);
|
|
113
|
+
const wb = sidebarWeight(kb);
|
|
114
|
+
if (wa !== wb) return wa - wb;
|
|
115
|
+
return ka.localeCompare(kb);
|
|
116
|
+
});
|
|
117
|
+
const walk = (item: Item, depth: number): Item | null => {
|
|
118
|
+
if (item.type === "doc") {
|
|
119
|
+
const tail = slugTail(item.id);
|
|
120
|
+
if (hiddenAnywhere.has(tail)) return null;
|
|
121
|
+
if (depth === 0 && hiddenAtRoot.has(tail)) return null;
|
|
122
|
+
if (!item.label && looksRawSlug(tail)) {
|
|
123
|
+
return { ...item, label: humanizeSidebarLabel(tail) };
|
|
124
|
+
}
|
|
125
|
+
return item;
|
|
126
|
+
}
|
|
127
|
+
if (item.type === "category") {
|
|
128
|
+
const label = looksRawSlug(item.label)
|
|
129
|
+
? humanizeSidebarLabel(item.label)
|
|
130
|
+
: item.label;
|
|
131
|
+
const children = item.items
|
|
132
|
+
.map((c: Item) => walk(c, depth + 1))
|
|
133
|
+
.filter((x: Item | null): x is Item => x !== null);
|
|
134
|
+
return {
|
|
135
|
+
...item,
|
|
136
|
+
label,
|
|
137
|
+
collapsed: depth === 0 ? false : item.collapsed,
|
|
138
|
+
items: children,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return item;
|
|
142
|
+
};
|
|
143
|
+
return sortTopLevel(
|
|
144
|
+
items
|
|
145
|
+
.map((c: Item) => walk(c, 0))
|
|
146
|
+
.filter((x: Item | null): x is Item => x !== null),
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
`
|
|
150
|
+
|
|
151
|
+
const markdownBlock = (mermaid: boolean): string =>
|
|
152
|
+
mermaid
|
|
153
|
+
? `markdown: {
|
|
154
|
+
mermaid: true,
|
|
155
|
+
format: "mdx",
|
|
156
|
+
},
|
|
157
|
+
`
|
|
158
|
+
: ""
|
|
159
|
+
|
|
160
|
+
const themesSection = (mermaid: boolean): string =>
|
|
161
|
+
mermaid ? ` themes: ["@docusaurus/theme-mermaid"],\n` : ""
|
|
162
|
+
|
|
163
|
+
const mermaidPrismTheme = (mermaid: boolean): string =>
|
|
164
|
+
mermaid
|
|
165
|
+
? `,
|
|
166
|
+
mermaid: { theme: { light: "neutral", dark: "dark" } }`
|
|
167
|
+
: ""
|
|
168
|
+
|
|
169
|
+
const repoNavItem = (repo: string | undefined): string =>
|
|
170
|
+
repo !== undefined && repo !== ""
|
|
171
|
+
? ` {
|
|
172
|
+
href: ${JSON.stringify(repo)},
|
|
173
|
+
position: "right",
|
|
174
|
+
className: "navbar__item navbar__item--github",
|
|
175
|
+
"aria-label": "View source on GitHub",
|
|
176
|
+
title: "GitHub",
|
|
177
|
+
label: "GitHub",
|
|
178
|
+
},
|
|
179
|
+
`
|
|
180
|
+
: ""
|
|
181
|
+
|
|
182
|
+
export const genDocusaurusConfig = (opts: {
|
|
183
|
+
readonly siteTitle: string
|
|
184
|
+
readonly tagline: string
|
|
185
|
+
readonly mermaid: boolean
|
|
186
|
+
readonly repo?: string | undefined
|
|
187
|
+
/** When true, emit the SpellbookChat webpack plugin (`__SPELLBOOK_ENABLED__`,
|
|
188
|
+
* SSR aliases for `@huggingface/transformers` / `@mlc-ai/web-llm`, asset rules). */
|
|
189
|
+
readonly spellbook?: boolean | undefined
|
|
190
|
+
}): string => {
|
|
191
|
+
const mb = markdownBlock(opts.mermaid)
|
|
192
|
+
const ts = themesSection(opts.mermaid)
|
|
193
|
+
const mpt = mermaidPrismTheme(opts.mermaid)
|
|
194
|
+
const rni = repoNavItem(opts.repo)
|
|
195
|
+
const spellbookOn = opts.spellbook === true
|
|
196
|
+
const pluginsList = spellbookOn
|
|
197
|
+
? "[grimoireDevWatchPlugin, spellbookWebpackPlugin]"
|
|
198
|
+
: "[grimoireDevWatchPlugin]"
|
|
199
|
+
|
|
200
|
+
return `${genImports(spellbookOn)}
|
|
201
|
+
// macOS EMFILE workaround when \`docs.path\` walks the whole tome: force
|
|
202
|
+
// Webpack/Watchpack and chokidar onto polling. Opt out with
|
|
203
|
+
// WATCHPACK_POLLING=false / CHOKIDAR_USEPOLLING=false.
|
|
204
|
+
${genWatcherEnv()}
|
|
205
|
+
${genDevWatchPlugin()}
|
|
206
|
+
// Sidebar polish: humanize raw slugs into Sentence-case, upper-case known
|
|
207
|
+
// technical acronyms, and weight common top-level slugs (getting-started
|
|
208
|
+
// first, architecture / internals last). Agent-authored \`sidebar_label\` /
|
|
209
|
+
// \`sidebar_position\` always wins over these fallbacks.
|
|
210
|
+
${genSidebarHelpers()}
|
|
211
|
+
const config: Config = {
|
|
212
|
+
title: ${JSON.stringify(opts.siteTitle)},
|
|
213
|
+
tagline: ${JSON.stringify(opts.tagline)},
|
|
214
|
+
favicon: "img/logo.svg",
|
|
215
|
+
url: "https://example.com",
|
|
216
|
+
baseUrl: "/",
|
|
217
|
+
organizationName: "grimoire",
|
|
218
|
+
projectName: "docs",
|
|
219
|
+
onBrokenLinks: "warn",
|
|
220
|
+
${mb} presets: [
|
|
221
|
+
[
|
|
222
|
+
"classic",
|
|
223
|
+
{
|
|
224
|
+
docs: {
|
|
225
|
+
path: "../",
|
|
226
|
+
exclude: [
|
|
227
|
+
"site/**",
|
|
228
|
+
"**/node_modules/**",
|
|
229
|
+
"**/.git/**",
|
|
230
|
+
".grimoire/**",
|
|
231
|
+
"**/.grimoire/**",
|
|
232
|
+
".grimoire-seal",
|
|
233
|
+
"**/.grimoire-seal",
|
|
234
|
+
".grimoire-progress.json",
|
|
235
|
+
"**/.grimoire-progress.json",
|
|
236
|
+
// README.md and index.md (slug: /) would both claim "/" — keep the
|
|
237
|
+
// landing page and exclude README globally.
|
|
238
|
+
"**/README.md",
|
|
239
|
+
],
|
|
240
|
+
routeBasePath: "/",
|
|
241
|
+
sidebarPath: "./sidebars.ts",
|
|
242
|
+
editLocalizedFiles: false,
|
|
243
|
+
editUrl: undefined,
|
|
244
|
+
${genSidebarItemsGenerator()} },
|
|
245
|
+
blog: false,
|
|
246
|
+
theme: {
|
|
247
|
+
customCss: "./src/css/custom.css",
|
|
248
|
+
},
|
|
249
|
+
} satisfies Preset.Options,
|
|
250
|
+
],
|
|
251
|
+
],
|
|
252
|
+
plugins: ${pluginsList},
|
|
253
|
+
${ts} themeConfig: {
|
|
254
|
+
colorMode: {
|
|
255
|
+
defaultMode: "light",
|
|
256
|
+
respectPrefersColorScheme: true,
|
|
257
|
+
},
|
|
258
|
+
navbar: {
|
|
259
|
+
title: ${JSON.stringify(opts.siteTitle)},
|
|
260
|
+
logo: {
|
|
261
|
+
alt: ${JSON.stringify(opts.siteTitle)},
|
|
262
|
+
src: "img/logo.svg",
|
|
263
|
+
srcDark: "img/logo-dark.svg",
|
|
264
|
+
},
|
|
265
|
+
items: [
|
|
266
|
+
${rni} ],
|
|
267
|
+
},
|
|
268
|
+
footer: {
|
|
269
|
+
style: "dark",
|
|
270
|
+
copyright: \`© \${new Date().getFullYear()} \${${JSON.stringify(opts.siteTitle)}}\`,
|
|
271
|
+
},
|
|
272
|
+
prism: {
|
|
273
|
+
theme: prismThemes.github,
|
|
274
|
+
darkTheme: prismThemes.dracula,
|
|
275
|
+
}${mpt},
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export default config;
|
|
280
|
+
`
|
|
281
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url"
|
|
2
|
+
import { siteScaffoldPhases } from "@kpritam/grimoire-core/services/SiteScaffold"
|
|
3
|
+
import { Effect, FileSystem, Path } from "effect"
|
|
4
|
+
|
|
5
|
+
// Loaded from `dist/internal/spellbookAssets.js`; `templates/spellbook` lives
|
|
6
|
+
// two levels up alongside the published `dist/` and `src/` folders.
|
|
7
|
+
const spellbookTemplatesRoot = (pathApi: Path.Path): string => {
|
|
8
|
+
const here = fileURLToPath(new URL(".", import.meta.url))
|
|
9
|
+
return pathApi.resolve(here, "..", "..", "templates", "spellbook")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Copy the SpellbookChat React tree, the `theme/Root.tsx` swizzle, the
|
|
14
|
+
* `SpellbookChatDisabled.tsx` stub, AND the root-level `spellbookPlugin.ts`
|
|
15
|
+
* into the scaffolded site. The template layout mirrors the site layout 1:1
|
|
16
|
+
* so this is a single recursive copy.
|
|
17
|
+
*
|
|
18
|
+
* Files are **overwritten** on every run. This is safe because the entire
|
|
19
|
+
* copied tree is package-managed: it's regenerated from
|
|
20
|
+
* `tome/site/src/components/SpellbookChat/` on every release of
|
|
21
|
+
* `@kpritam/grimoire-output-docusaurus` via `sync-spellbook-templates.mjs`.
|
|
22
|
+
* Customisation should happen in user-owned files (e.g. theme overrides),
|
|
23
|
+
* not by editing these in place.
|
|
24
|
+
*/
|
|
25
|
+
export const writeSpellbookAssets = (
|
|
26
|
+
fs: FileSystem.FileSystem,
|
|
27
|
+
pathApi: Path.Path,
|
|
28
|
+
siteDir: string
|
|
29
|
+
) =>
|
|
30
|
+
siteScaffoldPhases
|
|
31
|
+
.writeManagedAssets({
|
|
32
|
+
templatesRoot: spellbookTemplatesRoot(pathApi),
|
|
33
|
+
siteDir
|
|
34
|
+
})
|
|
35
|
+
.pipe(
|
|
36
|
+
Effect.provideService(FileSystem.FileSystem, fs),
|
|
37
|
+
Effect.provideService(Path.Path, pathApi)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* npm dependencies the SpellbookChat components require at runtime.
|
|
42
|
+
* Keep in sync with `tome/site/package.json` so the canonical site and
|
|
43
|
+
* scaffolded sites resolve identical versions.
|
|
44
|
+
*
|
|
45
|
+
* Notable additions over the v0.1.6 baseline:
|
|
46
|
+
* - `streamdown` replaces `react-markdown` for streaming-aware rendering
|
|
47
|
+
* - `@ricky0123/vad-web` + `onnxruntime-web` power Silero VAD for voice
|
|
48
|
+
* mode (Web Audio + ONNX in-browser)
|
|
49
|
+
*/
|
|
50
|
+
export const SPELLBOOK_DEPENDENCIES: Readonly<Record<string, string>> = {
|
|
51
|
+
"@ai-sdk/anthropic": "^3.0.77",
|
|
52
|
+
"@ai-sdk/google": "^3.0.73",
|
|
53
|
+
"@ai-sdk/openai": "^3.0.63",
|
|
54
|
+
"@ai-sdk/openai-compatible": "^2.0.47",
|
|
55
|
+
"@huggingface/transformers": "^4.2.0",
|
|
56
|
+
"@mlc-ai/web-llm": "^0.2.83",
|
|
57
|
+
"@ricky0123/vad-web": "^0.0.30",
|
|
58
|
+
ai: "^6.0.180",
|
|
59
|
+
"highlight.js": "^11.11.1",
|
|
60
|
+
"onnxruntime-web": "^1.26.0",
|
|
61
|
+
"rehype-highlight": "^7.0.2",
|
|
62
|
+
"remark-gfm": "^4.0.1",
|
|
63
|
+
streamdown: "^2.5.0"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build-time dependencies the Spellbook plugin imports directly.
|
|
68
|
+
*
|
|
69
|
+
* `spellbookPlugin.ts` calls `new webpack.NormalModuleReplacementPlugin(...)`
|
|
70
|
+
* to swap the chat tree for the disabled stub, so `webpack` must be
|
|
71
|
+
* resolvable both at type-check time (`tsc --noEmit`) and when Docusaurus
|
|
72
|
+
* loads the plugin. Strict pnpm node_modules layouts only expose packages
|
|
73
|
+
* that are direct dependencies, so we must declare it here even though
|
|
74
|
+
* `@docusaurus/core` already pulls it in transitively.
|
|
75
|
+
*
|
|
76
|
+
* Pinned to `^5.95.0` to match `@docusaurus/core@3.10.x`'s declared range.
|
|
77
|
+
*/
|
|
78
|
+
export const SPELLBOOK_DEV_DEPENDENCIES: Readonly<Record<string, string>> = {
|
|
79
|
+
webpack: "^5.95.0"
|
|
80
|
+
}
|
package/src/layer.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ScaffoldEnv, SiteGenerator } from "@kpritam/grimoire-core"
|
|
2
|
+
import { Effect, Layer } from "effect"
|
|
3
|
+
|
|
4
|
+
import { scaffoldUpstream } from "./upstream.js"
|
|
5
|
+
|
|
6
|
+
export const DocusaurusLayer: Layer.Layer<SiteGenerator, never, ScaffoldEnv> = Layer.succeed(
|
|
7
|
+
SiteGenerator,
|
|
8
|
+
{
|
|
9
|
+
scaffold: (opts) =>
|
|
10
|
+
scaffoldUpstream(opts).pipe(Effect.withSpan("grimoire.site.docusaurus.upstream"))
|
|
11
|
+
}
|
|
12
|
+
)
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PackageJsonPatches } from "@kpritam/grimoire-core/services/SiteScaffold"
|
|
2
|
+
import { genDocusaurusConfig as genDocusaurusConfigInner } from "./internal/docusaurusConfig.js"
|
|
3
|
+
import { SPELLBOOK_DEPENDENCIES, SPELLBOOK_DEV_DEPENDENCIES } from "./internal/spellbookAssets.js"
|
|
4
|
+
|
|
5
|
+
export const genDocusaurusConfig = genDocusaurusConfigInner
|
|
6
|
+
|
|
7
|
+
export const docusaurusPackagePatches = (
|
|
8
|
+
mermaid: boolean,
|
|
9
|
+
spellbook: boolean
|
|
10
|
+
): PackageJsonPatches => ({
|
|
11
|
+
private: true,
|
|
12
|
+
scripts: {
|
|
13
|
+
start: "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true docusaurus start",
|
|
14
|
+
serve: "CHOKIDAR_USEPOLLING=true WATCHPACK_POLLING=true docusaurus serve",
|
|
15
|
+
typecheck: "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
dependencies: {
|
|
18
|
+
...(mermaid
|
|
19
|
+
? {
|
|
20
|
+
"@docusaurus/theme-mermaid": "^3.10.0",
|
|
21
|
+
"@mermaid-js/layout-elk": "^0.1.9"
|
|
22
|
+
}
|
|
23
|
+
: {}),
|
|
24
|
+
...(spellbook ? SPELLBOOK_DEPENDENCIES : {})
|
|
25
|
+
},
|
|
26
|
+
devDependencies: spellbook ? SPELLBOOK_DEV_DEPENDENCIES : {}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const genSidebarsTs = (): string => {
|
|
30
|
+
return `import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
|
|
31
|
+
|
|
32
|
+
const sidebars: SidebarsConfig = {
|
|
33
|
+
grimoire: [
|
|
34
|
+
{
|
|
35
|
+
type: "autogenerated",
|
|
36
|
+
dirName: ".",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default sidebars;
|
|
42
|
+
`
|
|
43
|
+
}
|
package/src/upstream.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectPackageManager,
|
|
3
|
+
diagramEngineUsesMermaid,
|
|
4
|
+
outputWriteError,
|
|
5
|
+
partialScaffoldError,
|
|
6
|
+
type ScaffoldOptions,
|
|
7
|
+
type ScaffoldResult
|
|
8
|
+
} from "@kpritam/grimoire-core"
|
|
9
|
+
import { removeFiles, siteScaffoldPhases } from "@kpritam/grimoire-core/services/SiteScaffold"
|
|
10
|
+
import { Effect, FileSystem, Path } from "effect"
|
|
11
|
+
import { DOCUSAURUS_PLACEHOLDER_ASSETS, DOCUSAURUS_SAMPLE_RELPATHS } from "./internal/assets.js"
|
|
12
|
+
import { writeSpellbookAssets } from "./internal/spellbookAssets.js"
|
|
13
|
+
import { docusaurusPackagePatches, genDocusaurusConfig, genSidebarsTs } from "./shared.js"
|
|
14
|
+
|
|
15
|
+
/** Spawns `npx create-docusaurus`, applies Grimoire fix-up; skips full scaffold when `site/package.json` exists. */
|
|
16
|
+
export const scaffoldUpstream = (opts: ScaffoldOptions) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const fs = yield* FileSystem.FileSystem
|
|
19
|
+
const pathApi = yield* Path.Path
|
|
20
|
+
const siteTitle = opts.siteTitle ?? "Grimoire"
|
|
21
|
+
const tagline = opts.tagline ?? "Documentation generated by Grimoire"
|
|
22
|
+
const mermaidOn = diagramEngineUsesMermaid(opts.diagramEngine)
|
|
23
|
+
const spellbookOn = opts.spellbook === true
|
|
24
|
+
const logStream = opts.logStream === true
|
|
25
|
+
yield* fs
|
|
26
|
+
.makeDirectory(opts.tomeDir, { recursive: true })
|
|
27
|
+
.pipe(Effect.mapError((e) => outputWriteError(opts.tomeDir, e)))
|
|
28
|
+
|
|
29
|
+
const written: string[] = []
|
|
30
|
+
const pkgPath = pathApi.join(opts.siteDir, "package.json")
|
|
31
|
+
|
|
32
|
+
if (yield* siteScaffoldPhases.existing(opts.siteDir, pkgPath)) {
|
|
33
|
+
for (const p of yield* siteScaffoldPhases.writePlaceholderAssets(
|
|
34
|
+
opts.siteDir,
|
|
35
|
+
DOCUSAURUS_PLACEHOLDER_ASSETS
|
|
36
|
+
)) {
|
|
37
|
+
written.push(p)
|
|
38
|
+
}
|
|
39
|
+
if (spellbookOn) {
|
|
40
|
+
yield* siteScaffoldPhases.patchPackageJson(
|
|
41
|
+
pkgPath,
|
|
42
|
+
docusaurusPackagePatches(mermaidOn, spellbookOn)
|
|
43
|
+
)
|
|
44
|
+
written.push(pkgPath)
|
|
45
|
+
for (const p of yield* writeSpellbookAssets(fs, pathApi, opts.siteDir)) {
|
|
46
|
+
written.push(p)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { filesWritten: written } satisfies ScaffoldResult
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const siteDirExists = yield* fs.exists(opts.siteDir).pipe(Effect.orElseSucceed(() => false))
|
|
53
|
+
if (siteDirExists) {
|
|
54
|
+
return yield* Effect.fail(partialScaffoldError({ siteDir: opts.siteDir, pkgPath }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pm = yield* detectPackageManager(fs, pathApi, opts.root)
|
|
58
|
+
yield* siteScaffoldPhases.spawnUpstream({
|
|
59
|
+
binary: "npx",
|
|
60
|
+
args: [
|
|
61
|
+
"--yes",
|
|
62
|
+
"create-docusaurus@^3.10",
|
|
63
|
+
pathApi.basename(opts.siteDir),
|
|
64
|
+
"classic",
|
|
65
|
+
"--typescript",
|
|
66
|
+
"--skip-install",
|
|
67
|
+
"--package-manager",
|
|
68
|
+
pm
|
|
69
|
+
],
|
|
70
|
+
cwd: opts.tomeDir,
|
|
71
|
+
label: "docusaurus",
|
|
72
|
+
logStream
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
for (const p of yield* removeFiles(opts.siteDir, DOCUSAURUS_SAMPLE_RELPATHS)) {
|
|
76
|
+
written.push(`-${p}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
yield* siteScaffoldPhases.patchPackageJson(
|
|
80
|
+
pkgPath,
|
|
81
|
+
docusaurusPackagePatches(mermaidOn, spellbookOn)
|
|
82
|
+
)
|
|
83
|
+
written.push(pkgPath)
|
|
84
|
+
|
|
85
|
+
const docusaurusConfigPath = pathApi.join(opts.siteDir, "docusaurus.config.ts")
|
|
86
|
+
yield* siteScaffoldPhases.writeFrameworkConfig(
|
|
87
|
+
docusaurusConfigPath,
|
|
88
|
+
genDocusaurusConfig({
|
|
89
|
+
siteTitle,
|
|
90
|
+
tagline,
|
|
91
|
+
mermaid: mermaidOn,
|
|
92
|
+
spellbook: spellbookOn,
|
|
93
|
+
...(opts.siteRepo !== undefined ? { repo: opts.siteRepo } : {})
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
written.push(docusaurusConfigPath)
|
|
97
|
+
|
|
98
|
+
for (const p of yield* removeFiles(opts.siteDir, ["docusaurus.config.js", "sidebars.js"])) {
|
|
99
|
+
written.push(`-${p}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sidebarsPath = pathApi.join(opts.siteDir, "sidebars.ts")
|
|
103
|
+
yield* siteScaffoldPhases.writeFrameworkConfig(sidebarsPath, genSidebarsTs())
|
|
104
|
+
written.push(sidebarsPath)
|
|
105
|
+
|
|
106
|
+
for (const p of yield* siteScaffoldPhases.writePlaceholderAssets(
|
|
107
|
+
opts.siteDir,
|
|
108
|
+
DOCUSAURUS_PLACEHOLDER_ASSETS
|
|
109
|
+
)) {
|
|
110
|
+
written.push(p)
|
|
111
|
+
}
|
|
112
|
+
if (spellbookOn) {
|
|
113
|
+
for (const p of yield* writeSpellbookAssets(fs, pathApi, opts.siteDir)) {
|
|
114
|
+
written.push(p)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { filesWritten: written } satisfies ScaffoldResult
|
|
119
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import webpack from "webpack";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In-browser RAG chat ("Spellbook") master switch + Docusaurus plugin.
|
|
8
|
+
*
|
|
9
|
+
* This file is the single source of truth: the live Grimoire docs site
|
|
10
|
+
* imports it directly, and `@kpritam/grimoire-output-docusaurus` ships a
|
|
11
|
+
* mirror to scaffolded sites (see `templates/spellbook/spellbookPlugin.ts`
|
|
12
|
+
* and `sync-spellbook-templates.mjs`). Edit here, then re-run the sync.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
// Resolution order:
|
|
18
|
+
// 1. SPELLBOOK_ENABLED=true|false env var (explicit override)
|
|
19
|
+
// 2. Presence of static/grimoire-index/manifest.json (last `grimoire cast`)
|
|
20
|
+
// 3. Default: false
|
|
21
|
+
const SPELLBOOK_INDEX_MANIFEST = path.join(
|
|
22
|
+
__dirname,
|
|
23
|
+
"static/grimoire-index/manifest.json",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const resolveSpellbookEnabled = (): boolean => {
|
|
27
|
+
const envFlag = process.env.SPELLBOOK_ENABLED;
|
|
28
|
+
if (envFlag === "false" || envFlag === "0") return false;
|
|
29
|
+
if (envFlag === "true" || envFlag === "1") return true;
|
|
30
|
+
return fs.existsSync(SPELLBOOK_INDEX_MANIFEST);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const SPELLBOOK_ENABLED = resolveSpellbookEnabled();
|
|
34
|
+
|
|
35
|
+
const SPELLBOOK_DISABLED_STUB = path.join(
|
|
36
|
+
__dirname,
|
|
37
|
+
"src/components/SpellbookChatDisabled.tsx",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
interface MutableWebpackConfig {
|
|
41
|
+
resolve?: { alias?: Record<string, string> | unknown };
|
|
42
|
+
module?: { rules?: unknown[] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Webpack warnings we deliberately silence:
|
|
47
|
+
*
|
|
48
|
+
* - `onnxruntime-web` ships a UMD bundle whose dynamic `require()` only
|
|
49
|
+
* fires in non-web targets (Node fallbacks). Webpack can't statically
|
|
50
|
+
* resolve it, but it's effectively dead code in our browser build, so
|
|
51
|
+
* the "Critical dependency: require function is used in a way…" noise
|
|
52
|
+
* is purely cosmetic.
|
|
53
|
+
*
|
|
54
|
+
* - `@huggingface/transformers`'s web entry uses `import.meta` at the top
|
|
55
|
+
* level. Webpack 5 supports it but emits a "Critical dependency:
|
|
56
|
+
* Accessing import.meta directly is unsupported" warning because the
|
|
57
|
+
* runtime feature-detects gracefully. Library issue we can't fix here.
|
|
58
|
+
*
|
|
59
|
+
* Keeping this list narrow (matched by `module` and `message`) so future
|
|
60
|
+
* real warnings still surface.
|
|
61
|
+
*/
|
|
62
|
+
const SPELLBOOK_IGNORED_WARNINGS = [
|
|
63
|
+
{
|
|
64
|
+
module: /node_modules[\\/].*onnxruntime-web/,
|
|
65
|
+
message: /Critical dependency: require function is used in a way/,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
module:
|
|
69
|
+
/node_modules[\\/].*@huggingface[\\/]transformers[\\/].*transformers\.web/,
|
|
70
|
+
message:
|
|
71
|
+
/Critical dependency: Accessing import\.meta directly is unsupported/,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Replaces every entrypoint into the SpellbookChat tree with an inert
|
|
77
|
+
* stub when disabled (webpack still creates a chunk for `import()` even
|
|
78
|
+
* when the branch is dead at runtime), and wires SSR stubs + binary
|
|
79
|
+
* asset rules when enabled.
|
|
80
|
+
*
|
|
81
|
+
* Earlier revisions also defined a build-time `__SPELLBOOK_ENABLED__`
|
|
82
|
+
* constant via `webpack.DefinePlugin`. That triggered a webpack
|
|
83
|
+
* persistent-cache serialization warning ("No serializer registered for
|
|
84
|
+
* ConstDependency") on every consumer of the constant. Since
|
|
85
|
+
* `NormalModuleReplacementPlugin` already collapses the SpellbookChat
|
|
86
|
+
* graph to the inert stub when disabled — and tree-shaking handles the
|
|
87
|
+
* empty default export — the DefinePlugin layer was redundant. Removing
|
|
88
|
+
* it eliminates the cache warning without changing bundle output.
|
|
89
|
+
*/
|
|
90
|
+
export const spellbookWebpackPlugin = () => ({
|
|
91
|
+
name: "spellbook-webpack",
|
|
92
|
+
configureWebpack(config: MutableWebpackConfig, isServer: boolean) {
|
|
93
|
+
if (!SPELLBOOK_ENABLED) {
|
|
94
|
+
const replacePlugin = new webpack.NormalModuleReplacementPlugin(
|
|
95
|
+
/(?:^|[\\/])components[\\/]SpellbookChat(?:[\\/](?:index)?)?$/,
|
|
96
|
+
SPELLBOOK_DISABLED_STUB,
|
|
97
|
+
);
|
|
98
|
+
return { plugins: [replacePlugin] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const transformersStub = path.join(
|
|
102
|
+
__dirname,
|
|
103
|
+
"src/components/SpellbookChat/transformers-ssr-stub.ts",
|
|
104
|
+
);
|
|
105
|
+
const webllmStub = path.join(
|
|
106
|
+
__dirname,
|
|
107
|
+
"src/components/SpellbookChat/webllm-ssr-stub.ts",
|
|
108
|
+
);
|
|
109
|
+
const vadStub = path.join(
|
|
110
|
+
__dirname,
|
|
111
|
+
"src/components/SpellbookChat/vad-ssr-stub.ts",
|
|
112
|
+
);
|
|
113
|
+
const binaryRule = {
|
|
114
|
+
test: /\.(wasm|onnx|bin|node)$/i,
|
|
115
|
+
type: "asset/resource" as const,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (isServer) {
|
|
119
|
+
config.resolve ??= {};
|
|
120
|
+
const prev = config.resolve.alias;
|
|
121
|
+
const alias: Record<string, string> =
|
|
122
|
+
prev && typeof prev === "object" && !Array.isArray(prev)
|
|
123
|
+
? { ...(prev as Record<string, string>) }
|
|
124
|
+
: {};
|
|
125
|
+
alias["@huggingface/transformers"] = transformersStub;
|
|
126
|
+
alias["@mlc-ai/web-llm"] = webllmStub;
|
|
127
|
+
// `@ricky0123/vad-web` and `onnxruntime-web` are not safe to evaluate
|
|
128
|
+
// during Docusaurus SSR — they reference `AudioWorklet` and resolve
|
|
129
|
+
// .wasm assets. Stub the VAD entrypoint and let the binary rule
|
|
130
|
+
// handle the ORT wasm files when bundled for the browser.
|
|
131
|
+
alias["@ricky0123/vad-web"] = vadStub;
|
|
132
|
+
config.resolve.alias = alias;
|
|
133
|
+
config.module ??= { rules: [] };
|
|
134
|
+
config.module.rules ??= [];
|
|
135
|
+
config.module.rules.push(binaryRule);
|
|
136
|
+
return { ignoreWarnings: SPELLBOOK_IGNORED_WARNINGS };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
experiments: {
|
|
141
|
+
asyncWebAssembly: true,
|
|
142
|
+
},
|
|
143
|
+
resolve: {
|
|
144
|
+
fallback: {
|
|
145
|
+
fs: false as const,
|
|
146
|
+
path: false as const,
|
|
147
|
+
crypto: false as const,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
module: {
|
|
151
|
+
rules: [binaryRule],
|
|
152
|
+
},
|
|
153
|
+
ignoreWarnings: SPELLBOOK_IGNORED_WARNINGS,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
});
|