@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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/internal/assets.d.ts +9 -0
  7. package/dist/internal/assets.js +50 -0
  8. package/dist/internal/docusaurusConfig.d.ts +9 -0
  9. package/dist/internal/docusaurusConfig.js +259 -0
  10. package/dist/internal/spellbookAssets.d.ts +39 -0
  11. package/dist/internal/spellbookAssets.js +68 -0
  12. package/dist/layer.d.ts +3 -0
  13. package/dist/layer.js +6 -0
  14. package/dist/shared.d.ts +10 -0
  15. package/dist/shared.js +36 -0
  16. package/dist/upstream.d.ts +6 -0
  17. package/dist/upstream.js +84 -0
  18. package/package.json +59 -0
  19. package/src/index.ts +1 -0
  20. package/src/internal/assets.ts +66 -0
  21. package/src/internal/docusaurusConfig.ts +281 -0
  22. package/src/internal/spellbookAssets.ts +80 -0
  23. package/src/layer.ts +12 -0
  24. package/src/shared.ts +43 -0
  25. package/src/upstream.ts +119 -0
  26. package/templates/spellbook/spellbookPlugin.ts +156 -0
  27. package/templates/spellbook/src/components/SpellbookChat/ChatEngine.ts +79 -0
  28. package/templates/spellbook/src/components/SpellbookChat/ChatErrorBoundary.tsx +65 -0
  29. package/templates/spellbook/src/components/SpellbookChat/Markdown.tsx +259 -0
  30. package/templates/spellbook/src/components/SpellbookChat/README.md +111 -0
  31. package/templates/spellbook/src/components/SpellbookChat/SettingsPanel.tsx +376 -0
  32. package/templates/spellbook/src/components/SpellbookChat/VoiceMode.tsx +867 -0
  33. package/templates/spellbook/src/components/SpellbookChat/index.tsx +744 -0
  34. package/templates/spellbook/src/components/SpellbookChat/markdown.module.css +343 -0
  35. package/templates/spellbook/src/components/SpellbookChat/secretStore.ts +106 -0
  36. package/templates/spellbook/src/components/SpellbookChat/streamProviders/anthropic.ts +36 -0
  37. package/templates/spellbook/src/components/SpellbookChat/streamProviders/createCloudProvider.ts +112 -0
  38. package/templates/spellbook/src/components/SpellbookChat/streamProviders/google.ts +33 -0
  39. package/templates/spellbook/src/components/SpellbookChat/streamProviders/index.ts +32 -0
  40. package/templates/spellbook/src/components/SpellbookChat/streamProviders/mapFinishReason.ts +23 -0
  41. package/templates/spellbook/src/components/SpellbookChat/streamProviders/ollama.ts +44 -0
  42. package/templates/spellbook/src/components/SpellbookChat/streamProviders/openai.ts +34 -0
  43. package/templates/spellbook/src/components/SpellbookChat/streamProviders/openaiRealtime.ts +320 -0
  44. package/templates/spellbook/src/components/SpellbookChat/streamProviders/types.ts +172 -0
  45. package/templates/spellbook/src/components/SpellbookChat/streamProviders/webllm.ts +214 -0
  46. package/templates/spellbook/src/components/SpellbookChat/styles.module.css +852 -0
  47. package/templates/spellbook/src/components/SpellbookChat/systemPrompt.ts +107 -0
  48. package/templates/spellbook/src/components/SpellbookChat/transformers-ssr-stub.ts +16 -0
  49. package/templates/spellbook/src/components/SpellbookChat/types.ts +52 -0
  50. package/templates/spellbook/src/components/SpellbookChat/useBundleLoader.ts +46 -0
  51. package/templates/spellbook/src/components/SpellbookChat/useChatEngine.ts +524 -0
  52. package/templates/spellbook/src/components/SpellbookChat/useEmbeddings.ts +147 -0
  53. package/templates/spellbook/src/components/SpellbookChat/useRetrieval.ts +377 -0
  54. package/templates/spellbook/src/components/SpellbookChat/useSileroVAD.ts +236 -0
  55. package/templates/spellbook/src/components/SpellbookChat/useSpeechRecognition.ts +271 -0
  56. package/templates/spellbook/src/components/SpellbookChat/useSpeechSynthesis.ts +229 -0
  57. package/templates/spellbook/src/components/SpellbookChat/useUnifiedSTT.ts +134 -0
  58. package/templates/spellbook/src/components/SpellbookChat/useWhisperSTT.ts +411 -0
  59. package/templates/spellbook/src/components/SpellbookChat/vad-ssr-stub.ts +25 -0
  60. package/templates/spellbook/src/components/SpellbookChat/voiceDebug.ts +60 -0
  61. package/templates/spellbook/src/components/SpellbookChat/voiceFsm.ts +196 -0
  62. package/templates/spellbook/src/components/SpellbookChat/voiceStyles.module.css +334 -0
  63. package/templates/spellbook/src/components/SpellbookChat/webllm-ssr-stub.ts +8 -0
  64. package/templates/spellbook/src/components/SpellbookChatDisabled.tsx +20 -0
  65. 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
+ }
@@ -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
+ });