@writedocs/generator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +17 -0
  3. package/astro.config.mjs +419 -0
  4. package/bin/writedocs.js +73 -0
  5. package/package.json +79 -0
  6. package/src/assets/wd_watermark.png +0 -0
  7. package/src/assets/wd_watermark_dark.png +0 -0
  8. package/src/cli/build-auth.js +53 -0
  9. package/src/cli/build.js +40 -0
  10. package/src/cli/dev.js +12 -0
  11. package/src/cli/generate-api-pages.js +359 -0
  12. package/src/cli/init.js +81 -0
  13. package/src/cli/preflight.js +40 -0
  14. package/src/cli/run-astro.js +57 -0
  15. package/src/cli/run-pagefind.js +66 -0
  16. package/src/cli/write-redirects-file.js +80 -0
  17. package/src/components/Accordion.astro +164 -0
  18. package/src/components/AccordionGroup.astro +40 -0
  19. package/src/components/ApiLangSelect.astro +168 -0
  20. package/src/components/ApiPlayground.astro +281 -0
  21. package/src/components/ApiReferencePanel.astro +1754 -0
  22. package/src/components/ApiSchemaField.astro +54 -0
  23. package/src/components/AppIcon.astro +32 -0
  24. package/src/components/Badge.astro +128 -0
  25. package/src/components/Callout.astro +168 -0
  26. package/src/components/Card.astro +136 -0
  27. package/src/components/CardGroup.astro +20 -0
  28. package/src/components/CodeGroup.astro +184 -0
  29. package/src/components/CopyPageMenu.astro +246 -0
  30. package/src/components/Danger.astro +12 -0
  31. package/src/components/Expandable.astro +126 -0
  32. package/src/components/Frame.astro +102 -0
  33. package/src/components/Hint.astro +99 -0
  34. package/src/components/Icon.astro +70 -0
  35. package/src/components/Image.astro +147 -0
  36. package/src/components/Info.astro +12 -0
  37. package/src/components/Note.astro +12 -0
  38. package/src/components/Parameter.astro +119 -0
  39. package/src/components/RequestExample.astro +33 -0
  40. package/src/components/ResponseExample.astro +19 -0
  41. package/src/components/Searchbar.astro +117 -0
  42. package/src/components/Step.astro +10 -0
  43. package/src/components/Steps.astro +32 -0
  44. package/src/components/Tab.astro +9 -0
  45. package/src/components/Tabs.astro +52 -0
  46. package/src/components/Tip.astro +12 -0
  47. package/src/components/Video.astro +135 -0
  48. package/src/components/Warning.astro +12 -0
  49. package/src/components/index.ts +48 -0
  50. package/src/content.config.ts +223 -0
  51. package/src/layout/BaseLayout.astro +750 -0
  52. package/src/layout/components/AnalyticsScripts.astro +77 -0
  53. package/src/layout/components/AskAiWidget.astro +37 -0
  54. package/src/layout/components/Breadcrumbs.astro +97 -0
  55. package/src/layout/components/ImageZoom.astro +19 -0
  56. package/src/layout/components/MobileMenu.astro +200 -0
  57. package/src/layout/components/NavTree.astro +351 -0
  58. package/src/layout/components/SearchModal.astro +42 -0
  59. package/src/layout/components/Sidebar.astro +122 -0
  60. package/src/layout/components/SiteFooter.astro +85 -0
  61. package/src/layout/components/TableOfContents.astro +117 -0
  62. package/src/layout/components/TopBar.astro +311 -0
  63. package/src/layout/styles/banner.css +44 -0
  64. package/src/layout/styles/base.css +234 -0
  65. package/src/layout/styles/dropdown.css +133 -0
  66. package/src/layout/styles/footer.css +108 -0
  67. package/src/layout/styles/image-zoom.css +50 -0
  68. package/src/layout/styles/mobile-menu.css +258 -0
  69. package/src/layout/styles/search-modal.css +122 -0
  70. package/src/layout/styles/topbar.css +437 -0
  71. package/src/lib/config.ts +2131 -0
  72. package/src/lib/mdx-auto-hydrate.js +70 -0
  73. package/src/lib/mdx-inject-builtins.js +87 -0
  74. package/src/lib/mdx-substitute-variables.js +66 -0
  75. package/src/lib/mdx-title-anchor-ids.js +84 -0
  76. package/src/lib/mermaid-rehype.js +72 -0
  77. package/src/lib/openapi-render.ts +479 -0
  78. package/src/lib/shiki-code-block.js +102 -0
  79. package/src/lib/shiki-copy-button.js +45 -0
  80. package/src/lib/styles-asset-integration.js +210 -0
  81. package/src/lib/writedocs-temp-dir.js +93 -0
  82. package/src/pages/404.astro +62 -0
  83. package/src/pages/[...slug].astro +1270 -0
  84. package/src/pages/[...slug].md.ts +78 -0
  85. package/src/pages/llms-full.txt.ts +71 -0
  86. package/src/pages/llms.txt.ts +141 -0
  87. package/src/scripts/banner.ts +20 -0
  88. package/src/scripts/dropdowns.ts +61 -0
  89. package/src/scripts/image-zoom.ts +66 -0
  90. package/src/scripts/mobile-menu.ts +55 -0
  91. package/src/scripts/search.ts +155 -0
  92. package/src/scripts/sidebar-scroll.ts +65 -0
  93. package/src/scripts/theme-toggle.ts +35 -0
  94. package/src/scripts/topbar-offset.ts +141 -0
  95. package/src/styles/global.css +18 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Gabriel Raeder
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # Writedocs
2
+
3
+ A static site generator for documentation: a `writedocs.json` config + a folder of MDX/Markdown in, a fully static site out. Built on [Astro](https://astro.build), in the spirit of Docusaurus/Mintlify.
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ npm install -g @writedocs/generator
9
+
10
+ cd my-docs # any folder containing writedocs.json + docs/
11
+ writedocs dev
12
+ writedocs build
13
+ ```
14
+
15
+ The npm package is `@writedocs/generator`; the CLI command it installs is still `writedocs` (see the `bin` field in `package.json`).
16
+
17
+ `npx @writedocs/generator init/dev/build` still works too, for a one-off run with no install at all.
@@ -0,0 +1,419 @@
1
+ import { defineConfig } from 'astro/config';
2
+ import mdx from '@astrojs/mdx';
3
+ import react from '@astrojs/react';
4
+ import icon from 'astro-icon';
5
+ import sitemap from '@astrojs/sitemap';
6
+ import tailwindcss from '@tailwindcss/vite';
7
+ import path from 'node:path';
8
+ import fs from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import matter from 'gray-matter';
11
+ import { unified } from '@astrojs/markdown-remark';
12
+ import {
13
+ transformerMetaHighlight,
14
+ transformerMetaWordHighlight,
15
+ transformerNotationHighlight,
16
+ transformerNotationWordHighlight,
17
+ transformerNotationFocus,
18
+ transformerNotationDiff,
19
+ transformerNotationErrorLevel,
20
+ } from '@shikijs/transformers';
21
+ import { codeBlockTransformer } from './src/lib/shiki-code-block.js';
22
+ import { rehypeMermaid } from './src/lib/mermaid-rehype.js';
23
+ import remarkMath from 'remark-math';
24
+ import rehypeKatex from 'rehype-katex';
25
+ import { remarkAutoHydrateSnippets } from './src/lib/mdx-auto-hydrate.js';
26
+ import { remarkInjectBuiltinComponents } from './src/lib/mdx-inject-builtins.js';
27
+ import { remarkTitleAnchorIds } from './src/lib/mdx-title-anchor-ids.js';
28
+ import { remarkSubstituteVariables } from './src/lib/mdx-substitute-variables.js';
29
+ import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
30
+ import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
31
+ import {
32
+ loadDocsConfig,
33
+ resolveCodeblockTheme,
34
+ resolveCodeblockLangAlias,
35
+ resolveSiteUrl,
36
+ normalizeEntryId,
37
+ findAllPages,
38
+ } from './src/lib/config.ts';
39
+
40
+ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
41
+ // See writedocsBuildStagingDir()'s own comment (writedocs-temp-dir.js) for
42
+ // why `outDir` below points here instead of directly at contentDir/dist -
43
+ // short version: keeps Astro's own build executing inside the package (so
44
+ // its dependencies' own module resolution keeps working), while build.js
45
+ // does the actual cross-filesystem-safe copy into contentDir/dist as an
46
+ // explicit final step once the build itself is done.
47
+ const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || path.dirname(fileURLToPath(import.meta.url));
48
+
49
+ // Read here (rather than deferred to page-render time, where
50
+ // loadDocsConfig() is also called from [...slug].astro) specifically so
51
+ // a site's writedocs.json can pick its own Shiki theme names - astro.config.mjs
52
+ // is where Shiki itself actually gets configured, and dev.js/build.js
53
+ // already guarantee writedocs.json exists and is valid JSON (preflightCheck())
54
+ // before runAstro() ever gets this far, so letting loadDocsConfig() throw
55
+ // its own validation error on a malformed writedocs.json here is consistent
56
+ // with how every other consumer of it in this codebase behaves - no
57
+ // special-casing needed.
58
+ const docsConfig = loadDocsConfig(contentDir);
59
+ // resolveCodeblockTheme() is the single source of truth for the
60
+ // github-light/github-dark fallback - also used by ApiReferencePanel.astro
61
+ // for its own separate <Code/> usages (which don't inherit
62
+ // markdown.shikiConfig at all), so both stay in sync off the same
63
+ // writedocs.json field.
64
+ const codeblockTheme = resolveCodeblockTheme(docsConfig);
65
+ // resolveCodeblockLangAlias() - see its own comment in lib/config.ts for
66
+ // why this exists: some bundled Shiki grammars (mdx, notably) parse
67
+ // without error but produce no real token differentiation under this
68
+ // theme, so a ```lang fence renders as one flat color instead of
69
+ // highlighted. Remaps a fence's language tag to a better-behaved grammar
70
+ // before Shiki ever sees it - built-in mdx -> jsx default, plus whatever
71
+ // a site adds of its own under writedocs.json's styles.codeblocks.langAlias.
72
+ const codeblockLangAlias = resolveCodeblockLangAlias(docsConfig);
73
+
74
+ // publicDir defaults relative to Astro's --root (always the writedocs
75
+ // package itself - see src/cli/run-astro.js), not the content directory
76
+ // being built. Point it at <contentDir>/public instead so a site's own
77
+ // static assets (logos, favicons, ...) actually get copied/served -
78
+ // falling back to Astro's normal default (<packageRoot>/public, which
79
+ // doesn't exist in this package) when a site has no public/ of its own,
80
+ // since passing a nonexistent publicDir is a hard Astro error.
81
+ const contentPublicDir = path.join(contentDir, 'public');
82
+
83
+ // Sitemap generation needs an absolute origin to build absolute <loc>
84
+ // entries against - writedocs.json's `domain` field (see resolveSiteUrl() in
85
+ // lib/config.ts) is the single source of truth for that, shared with the
86
+ // canonical/OG/Twitter URL tags BaseLayout.astro renders per-page. A site
87
+ // with no `domain` set (most fixtures, local-only builds) just doesn't
88
+ // get a sitemap.xml at all - @astrojs/sitemap requires Astro's own `site`
89
+ // config to be set and would otherwise throw, and a sitemap of
90
+ // document-relative URLs isn't meaningful anyway.
91
+ const siteUrl = resolveSiteUrl(docsConfig);
92
+ if (!siteUrl) {
93
+ console.log('[writedocs] No writedocs.json "domain" set - skipping sitemap.xml generation.');
94
+ }
95
+
96
+ /** Recursively lists every .md/.mdx file under `baseDir` (Node 20's
97
+ * recursive readdir, no extra glob dependency needed - matches the
98
+ * plain-fs-walk style already used by generate-api-pages.js's
99
+ * findHandWrittenOverrides()), or an empty list if the directory doesn't
100
+ * exist (e.g. a site with no OpenAPI groups has no generated-docs/ at
101
+ * all under writedocsTempDir()). */
102
+ function walkMdFiles(baseDir) {
103
+ if (!fs.existsSync(baseDir)) return [];
104
+ return fs
105
+ .readdirSync(baseDir, { recursive: true })
106
+ .filter((f) => /\.mdx?$/i.test(f))
107
+ .map((f) => path.join(baseDir, f));
108
+ }
109
+
110
+ /** The set of normalized page ids (see normalizeEntryId()) whose
111
+ * frontmatter sets `seo.noindex: true` - excluded from sitemap.xml below,
112
+ * since listing a page there while also telling crawlers not to index it
113
+ * (BaseLayout.astro's <meta name="robots" content="noindex..."> for the
114
+ * same flag) would be self-contradictory. Reads frontmatter directly via
115
+ * gray-matter rather than astro:content, which doesn't exist yet this
116
+ * early in Astro's own startup - the same reason
117
+ * generate-api-pages.js's own frontmatter scanning does the same thing.
118
+ *
119
+ * Reuses findAllPages() from lib/config.ts (rather than its own
120
+ * hand-rolled walk) for the hand-written-page half of this scan, so it
121
+ * always sees exactly the same file set that actually becomes a page in
122
+ * the `pages` collection - no risk of the two independently drifting
123
+ * apart on which directories get excluded. The fallback id
124
+ * (`relativeId` with its extension and any trailing `/index` segment
125
+ * stripped) mirrors fileIdForEntry()'s own algorithm in lib/config.ts,
126
+ * for a page with no explicit `slug` override. */
127
+ function collectNoindexIds(rootContentDir) {
128
+ const generatedDocsDir = path.join(writedocsTempDir(rootContentDir), 'generated-docs');
129
+ const ids = new Set();
130
+
131
+ for (const relativeId of findAllPages(rootContentDir)) {
132
+ const file = path.join(rootContentDir, relativeId);
133
+ const { data } = matter(fs.readFileSync(file, 'utf-8'));
134
+ if (!data?.seo?.noindex) continue;
135
+ const fallbackId = relativeId.replace(/\.mdx?$/i, '').replace(/\/index$/, '');
136
+ ids.add(normalizeEntryId(data.slug ?? fallbackId));
137
+ }
138
+ // Generated OpenAPI stub pages always set an explicit `slug` (see
139
+ // generate-api-pages.js) - there's no file-path-derived id to fall back
140
+ // to the way there is for a hand-written page.
141
+ for (const file of walkMdFiles(generatedDocsDir)) {
142
+ const { data } = matter(fs.readFileSync(file, 'utf-8'));
143
+ if (data?.seo?.noindex && data.slug) ids.add(normalizeEntryId(data.slug));
144
+ }
145
+ return ids;
146
+ }
147
+
148
+ const noindexIds = siteUrl ? collectNoindexIds(contentDir) : new Set();
149
+
150
+ // writedocs.json's `redirects` array -> Astro's own `redirects` config option
151
+ // (a plain `{ [source]: destination }` record - see redirectSchema in
152
+ // lib/config.ts for why there's no `permanent`/status-code field: this
153
+ // package always builds `output: 'static'` with no adapter installed, and
154
+ // Astro's own docs say a static redirect is always a client-side
155
+ // `<meta http-equiv="refresh">` page regardless of what status is asked
156
+ // for). Omitted entirely (rather than passed as `{}`) when a site has no
157
+ // redirects configured, matching the `siteUrl ? [...] : []` pattern above
158
+ // of only opting into a feature's config surface when it's actually used.
159
+ const redirectsConfig = Object.fromEntries(docsConfig.redirects.map((r) => [r.source, r.destination]));
160
+
161
+ export default defineConfig({
162
+ ...(siteUrl ? { site: siteUrl } : {}),
163
+ ...(docsConfig.redirects.length ? { redirects: redirectsConfig } : {}),
164
+ // Astro's own dev-mode toolbar (the floating pill in the bottom center
165
+ // of every page - inspect/audit/settings) is a tool for authoring
166
+ // Astro itself, not something a writedocs site's own reader/author
167
+ // should see while running `writedocs dev` - it's not exposed as a
168
+ // writedocs.json option (nothing site-specific to configure here) since
169
+ // there's no scenario where a writedocs project actually wants it on.
170
+ // Off in every environment (build never shows it anyway, since it's a
171
+ // dev-only overlay Astro strips from production output regardless).
172
+ devToolbar: { enabled: false },
173
+ // Not contentDir/dist directly - see writedocsBuildStagingDir()'s own
174
+ // comment (writedocs-temp-dir.js) for why, and build.js for the copy
175
+ // step that actually gets a finished build from here into the real
176
+ // <contentDir>/dist a reader/host ever sees. Irrelevant to `astro dev`
177
+ // (no build output involved), so this applies unconditionally rather
178
+ // than branching on which CLI command is running.
179
+ outDir: writedocsBuildStagingDir(packageRoot, contentDir),
180
+ cacheDir: path.join(writedocsTempDir(contentDir), 'cache'),
181
+ ...(fs.existsSync(contentPublicDir) ? { publicDir: contentPublicDir } : {}),
182
+ // icon() reads from whatever @iconify-json/* collections are installed
183
+ // (see package.json) and inlines only the specific icons a build
184
+ // actually references as <svg> - the installed collections don't
185
+ // affect output size beyond that. No `include` config needed: astro-icon
186
+ // resolves "collection:icon-name" against any installed collection
187
+ // automatically.
188
+ integrations: [
189
+ // markdown.processor (below) carries remarkAutoHydrateSnippets - see
190
+ // its own comment there for why it's attached there and not here.
191
+ // Auto-adds `client:load` to every usage of a component imported from
192
+ // a .jsx/.tsx file (a snippet - see docs/dev/docs/snippets.mdx) that
193
+ // doesn't already carry its own `client:*` attribute, so a snippet
194
+ // just works when dropped into a page - nobody using it needs to know
195
+ // Astro's hydration-directive syntax exists at all, only someone who
196
+ // wants a *different* strategy (client:visible/idle/only) needs to
197
+ // write one explicitly.
198
+ mdx(),
199
+ // Lets a site's own snippets (see docs/dev/docs/snippets.mdx) be real
200
+ // .jsx/.tsx React components - hooks and all - dropped straight into
201
+ // MDX and hydrated client-side as an Astro island (automatically -
202
+ // see remarkAutoHydrateSnippets above). writedocs' own built-in
203
+ // components (Callout, Card, ...) stay plain .astro files - this is
204
+ // purely for snippets a site author writes themselves.
205
+ react(),
206
+ icon(),
207
+ ...(siteUrl
208
+ ? [
209
+ sitemap({
210
+ filter: (page) => {
211
+ const pathname = new URL(page).pathname;
212
+ const id = normalizeEntryId(pathname);
213
+ return !noindexIds.has(id);
214
+ },
215
+ }),
216
+ ]
217
+ : []),
218
+ // Lets styles.favicon/styles.logo/styles.background.images/seo.ogImage/
219
+ // styles.fonts.*.source resolve from anywhere in the project, not just
220
+ // public/ - see collectConfiguredAssetPaths() (lib/config.ts) for the
221
+ // field list and styles-asset-integration.js for how it's actually
222
+ // served/copied.
223
+ stylesAssetFallback(contentDir),
224
+ ],
225
+ output: 'static',
226
+ // Dual Shiki themes for fenced code blocks (```) in MDX content, so
227
+ // they switch with the site's own [data-theme] toggle instead of
228
+ // always rendering Astro's single default (github-dark) regardless of
229
+ // light/dark mode. Defaults to github-light/github-dark, overridable
230
+ // per-site via writedocs.json's styles.codeblocks.{light,dark} (any Shiki
231
+ // theme *name* - see https://shiki.style/themes for the built-in
232
+ // list - not a custom theme object/JSON file, at least for now).
233
+ // Astro bakes the "light" theme's colors as the element's base inline
234
+ // style and adds --shiki-dark/--shiki-dark-bg (etc.) custom
235
+ // properties alongside it - see the
236
+ // [data-theme='dark'] :global(.astro-code) override in
237
+ // BaseLayout.astro that actually swaps to them.
238
+ markdown: {
239
+ // Excludes ```mermaid fences from Shiki's own tokenization entirely -
240
+ // they reach rehypeMermaid (below) as plain, un-highlighted text,
241
+ // which is what mermaid.js needs to parse and render them into an
242
+ // actual diagram client-side (see [...slug].astro's initMermaid()).
243
+ // Syntax-highlighting mermaid's own pseudo-language as if it were
244
+ // source code would be actively wrong here - the goal is a rendered
245
+ // diagram, not colored diagram-description text.
246
+ syntaxHighlight: { type: 'shiki', excludeLangs: ['mermaid'] },
247
+ // Astro 7's markdown.rehypePlugins (top-level) is deprecated in favor
248
+ // of passing plugins straight to the `unified` processor itself -
249
+ // functionally identical (Astro's own deprecated-option shim used to
250
+ // just forward the top-level array into this exact processor), just
251
+ // explicit instead of relying on that shim (and the console warning
252
+ // it prints on every build/dev run). Astro still runs this *after*
253
+ // its own internal Shiki step (see @astrojs/markdown-remark's
254
+ // index.js) regardless of which processor plugins are attached to,
255
+ // so rehypeMermaid still always sees the excluded ```mermaid block
256
+ // completely untouched by Shiki - exactly the plain
257
+ // <pre><code class="language-mermaid"> shape it expects to reshape
258
+ // into mermaid.js's <div class="mermaid"> form.
259
+ // remarkInjectBuiltinComponents/remarkAutoHydrateSnippets only ever
260
+ // match JSX/import nodes that exist solely in MDX's extended syntax
261
+ // tree (see mdx-inject-builtins.js/mdx-auto-hydrate.js) - running them
262
+ // here rather than passing them to mdx({ remarkPlugins }) avoids that
263
+ // option's own deprecation warning ("pass it to unified()... MDX will
264
+ // inherit it" - extendMarkdownConfig defaults to true, so mdx() below
265
+ // already picks this processor up automatically). Harmless no-op on
266
+ // plain .md content, which never has an mdxjsEsm/mdxJsxFlowElement
267
+ // node to match in the first place.
268
+ // remarkSubstituteVariables is a parameterized plugin (an "attacher"
269
+ // that takes writedocs.json's `variables` map as its options and returns
270
+ // the actual transformer) - passed as a unified [attacher, options]
271
+ // tuple, *not* pre-called (`remarkSubstituteVariables(docsConfig.variables)`
272
+ // directly in this array). unified's own `use()` always calls
273
+ // whatever it finds in this array as if it were the attacher itself;
274
+ // handing it an already-produced transformer function makes unified
275
+ // invoke *that* with zero arguments during its `freeze()` setup pass,
276
+ // which crashes deep inside unist-util-visit ("Cannot use 'in'
277
+ // operator to search for 'children' in undefined") the first time any
278
+ // MDX page actually builds - a real bug hit and fixed while testing
279
+ // 25-site-config, not a hypothetical. remarkInjectBuiltinComponents/
280
+ // remarkAutoHydrateSnippets don't need this tuple form since neither
281
+ // takes options - they're valid attachers as bare function references.
282
+ // A site with no `variables` set still gets this plugin instance
283
+ // (which then no-ops on every file - see its own early-return) rather
284
+ // than being conditionally omitted, keeping this array's shape static
285
+ // regardless of writedocs.json content.
286
+ processor: unified({
287
+ // remarkMath goes first: it parses $inline$ and standalone $$block$$
288
+ // math into their own `inlineMath`/`math` mdast node types (mdast-
289
+ // util-to-hast has built-in support for both, turning them into a
290
+ // <span>/<div> carrying the raw LaTeX source as a "language-math"
291
+ // code-like payload for rehypeKatex, below, to render). Running it
292
+ // before remarkSubstituteVariables means a $...$ span's raw LaTeX
293
+ // source is no longer a plain `text` node by the time that plugin's
294
+ // visitor runs (it only visits `text` nodes - see its own comment) -
295
+ // not that `[[key]]` syntax inside actual LaTeX is a realistic thing
296
+ // to write, but this keeps the two plugins' node-type boundaries
297
+ // clean rather than leaving it to chance which one sees a math span
298
+ // as raw text first.
299
+ remarkPlugins: [
300
+ remarkMath,
301
+ remarkInjectBuiltinComponents,
302
+ // Runs after remarkInjectBuiltinComponents (order doesn't actually
303
+ // matter between them - that plugin only ever prepends an import
304
+ // statement, never touches an existing JSX node's attributes) but
305
+ // grouped right next to it since both deal with the same set of
306
+ // built-in component tag names. See its own comment for why this
307
+ // has to be a whole-document pass rather than logic living inside
308
+ // Callout.astro/Accordion.astro themselves.
309
+ remarkTitleAnchorIds,
310
+ remarkAutoHydrateSnippets,
311
+ [remarkSubstituteVariables, docsConfig.variables],
312
+ ],
313
+ // rehypeKatex renders every math/inlineMath node remarkMath produced
314
+ // into real KaTeX markup (a <span class="katex">...</span> tree of
315
+ // MathML + HTML, not an image or client-rendered widget) - fully at
316
+ // build time, so unlike Mermaid there's no client-side JS or runtime
317
+ // cost at all, just the one katex.min.css import (BaseLayout.astro)
318
+ // for layout/glyph styling. Order relative to rehypeMermaid doesn't
319
+ // matter - they walk disjoint hast node shapes (pre>code.language-
320
+ // mermaid vs. remarkMath's own math/inlineMath-derived nodes) and
321
+ // neither plugin's output is visible to the other.
322
+ rehypePlugins: [rehypeMermaid, rehypeKatex],
323
+ }),
324
+ shikiConfig: {
325
+ themes: codeblockTheme,
326
+ langAlias: codeblockLangAlias,
327
+ // The official @shikijs/transformers annotation transformers -
328
+ // enabled sitewide so any fenced code block can use their meta-
329
+ // string/comment syntax without per-page opt-in:
330
+ // ```js {1,3-5} -> transformerMetaHighlight
331
+ // ```js /someWord/ -> transformerMetaWordHighlight
332
+ // // [!code highlight] -> transformerNotationHighlight
333
+ // // [!code word:someWord] -> transformerNotationWordHighlight
334
+ // // [!code focus] -> transformerNotationFocus
335
+ // // [!code ++] / // [!code --] -> transformerNotationDiff
336
+ // // [!code error] / [!code warning] -> transformerNotationErrorLevel
337
+ // (comment syntax adapts to the language - # for Python/bash, //
338
+ // for JS/etc - see each transformer's own docs at shiki.style).
339
+ // Plus codeBlockTransformer, writedocs' own transformer for the
340
+ // copy button, an optional title bar, and wrap/lines/expandable
341
+ // meta flags - see src/lib/shiki-code-block.js. It runs last so
342
+ // its `root` hook wraps the <pre> only after every class the
343
+ // transformers above added to it is already in place - though in
344
+ // practice this doesn't matter, since all `pre`/`line` hooks
345
+ // across every transformer run to completion before any `root`
346
+ // hook runs at all (two separate passes, not interleaved).
347
+ transformers: [
348
+ transformerMetaHighlight(),
349
+ transformerMetaWordHighlight(),
350
+ transformerNotationHighlight(),
351
+ transformerNotationWordHighlight(),
352
+ transformerNotationFocus(),
353
+ transformerNotationDiff(),
354
+ transformerNotationErrorLevel(),
355
+ codeBlockTransformer(),
356
+ ],
357
+ },
358
+ },
359
+ // Tailwind is available for future/custom content (e.g. hand-authored
360
+ // MDX) but nothing in writedocs' own components has been migrated to
361
+ // it - they still use scoped <style> blocks + the --wd-* CSS variables
362
+ // so per-site writedocs.json theming (colors, and now dark mode) keeps
363
+ // working. See src/styles/global.css for the one @import that wires
364
+ // Tailwind's utilities in.
365
+ vite: {
366
+ plugins: [tailwindcss()],
367
+ resolve: {
368
+ alias: [
369
+ // Lets a page's MDX write `import Foo from '/snippets/foo.mdx'`
370
+ // (Mintlify's own snippet-import convention) instead of a
371
+ // relative path whose "../" depth depends on how deeply nested
372
+ // the importing page happens to be - see docs/dev/docs/snippets.mdx
373
+ // for the full feature writeup. Vite's project root is always
374
+ // packageRoot (this package itself - see run-astro.js), not
375
+ // contentDir, so a real leading-slash import would otherwise
376
+ // resolve (and fail to find anything) inside this package
377
+ // instead of the site's own content directory; this alias
378
+ // redirects just that one prefix to contentDir/snippets/ instead.
379
+ { find: /^\/snippets\//, replacement: path.join(contentDir, 'snippets') + '/' },
380
+ // remarkInjectBuiltinComponents (mdx-inject-builtins.js) auto-
381
+ // injects `import { Callout, ... } from 'writedocs/components'`
382
+ // into any .mdx file that uses a built-in component tag without
383
+ // importing it itself - a real bare specifier Vite has to resolve
384
+ // as if the .mdx file had written it. package.json's own
385
+ // `exports: { "./components": "./src/components/index.ts" }`
386
+ // makes that specifier valid *in principle* (Node's package
387
+ // self-reference feature - a package importing its own name), but
388
+ // self-reference only resolves when the importing file is a
389
+ // descendant of this package's own package.json - true for every
390
+ // existing fixture (always built from inside this repo/package
391
+ // tree) but never true for a real consumer's content, which lives
392
+ // in a completely separate directory in the actual product
393
+ // architecture (see docs/dev/docs/architecture.mdx) - walking up
394
+ // from such a file never finds a package.json named "writedocs",
395
+ // so self-reference fails and Vite reports the bare specifier as
396
+ // unresolvable ("Rolldown failed to resolve import
397
+ // 'writedocs/components'"). See BUG.md for the full writeup this
398
+ // fix comes from.
399
+ // This alias bypasses self-reference resolution entirely for
400
+ // that one specifier - Vite's alias matching is a plain string
401
+ // match against the specifier itself, independent of which file
402
+ // imported it, so it resolves correctly regardless of where the
403
+ // content directory actually lives. WRITEDOCS_PACKAGE_ROOT is the
404
+ // same packageRoot value already set by run-astro.js for
405
+ // lib/config.ts's fileIdForEntry() to consume - reusing it here
406
+ // instead of recomputing via import.meta.url keeps there being
407
+ // exactly one source of truth for "where this package lives" per
408
+ // astro subprocess. Falls back to this file's own directory
409
+ // (astro.config.mjs always lives at packageRoot) for anything
410
+ // that ever imports this config outside that subprocess, e.g. a
411
+ // future test harness.
412
+ {
413
+ find: 'writedocs/components',
414
+ replacement: path.join(packageRoot, 'src/components/index.ts'),
415
+ },
416
+ ],
417
+ },
418
+ },
419
+ });
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import dotenv from 'dotenv';
7
+ import { runDev } from '../src/cli/dev.js';
8
+ import { runBuild } from '../src/cli/build.js';
9
+ import { runInit } from '../src/cli/init.js';
10
+ import { requireBuildKey } from '../src/cli/build-auth.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const packageRoot = path.resolve(__dirname, '..');
14
+ // Read rather than hardcode - a literal version string here silently drifts
15
+ // from package.json's own "version" the moment either one is bumped without
16
+ // the other (exactly what `npm version <bump>` does: it only touches
17
+ // package.json). `writedocs --version` should always reflect what actually
18
+ // got published, not whatever this string happened to say at the time this
19
+ // line was last hand-edited.
20
+ const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
21
+
22
+ const program = new Command();
23
+ program
24
+ .name('writedocs')
25
+ .description('Static site generator for writedocs.json + MDX')
26
+ .version(version);
27
+
28
+ program
29
+ .command('dev')
30
+ .description('Start a local dev server with hot reload')
31
+ .argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
32
+ .option('-p, --port <port>', 'port to run on')
33
+ .action(async (dir, opts) => {
34
+ await runDev({
35
+ contentDir: path.resolve(process.cwd(), dir),
36
+ packageRoot,
37
+ port: opts.port,
38
+ });
39
+ });
40
+
41
+ program
42
+ // { hidden: true } keeps `build` out of `writedocs --help` - it's not a
43
+ // command ordinary users are meant to discover or run themselves. See
44
+ // src/cli/build-auth.js for the second layer: even someone who knows the
45
+ // command exists still can't run it without the right key.
46
+ .command('build', { hidden: true })
47
+ .description('Build a static site into <dir>/dist')
48
+ .argument('[dir]', 'content directory (contains writedocs.json and docs/)', '.')
49
+ .option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
50
+ .action(async (dir, opts) => {
51
+ const contentDir = path.resolve(process.cwd(), dir);
52
+ // Project-scoped, not global: a .env sitting next to this project's own
53
+ // writedocs.json (WRITEDOCS_KEY_SERVER_URL, WRITEDOCS_API_KEY) is picked
54
+ // up automatically, so build doesn't need those exported by hand every
55
+ // session. dotenv never overwrites a var already set in the real
56
+ // environment - an explicit `export`/CI secret still wins over the file.
57
+ dotenv.config({ path: path.join(contentDir, '.env'), quiet: true });
58
+ await requireBuildKey(opts.key || process.env.WRITEDOCS_API_KEY);
59
+ await runBuild({ contentDir, packageRoot });
60
+ });
61
+
62
+ program
63
+ .command('init')
64
+ .description('Scaffold a writedocs.json and starter docs/ folder')
65
+ .argument('[dir]', 'directory to scaffold into', '.')
66
+ .action(async (dir) => {
67
+ await runInit({ targetDir: path.resolve(process.cwd(), dir) });
68
+ });
69
+
70
+ program.parseAsync(process.argv).catch((err) => {
71
+ console.error(`[writedocs] ${err.message}`);
72
+ process.exit(1);
73
+ });
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@writedocs/generator",
3
+ "version": "0.1.0",
4
+ "description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
5
+ "type": "module",
6
+ "bin": {
7
+ "writedocs": "./bin/writedocs.js"
8
+ },
9
+ "exports": {
10
+ "./components": "./src/components/index.ts"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "astro.config.mjs"
16
+ ],
17
+ "scripts": {
18
+ "dev": "node bin/writedocs.js dev",
19
+ "build": "node bin/writedocs.js build",
20
+ "init": "node bin/writedocs.js init"
21
+ },
22
+ "keywords": [
23
+ "docs",
24
+ "documentation",
25
+ "static-site-generator",
26
+ "mdx",
27
+ "astro"
28
+ ],
29
+ "homepage": "https://github.com/writedocs/writedocs#readme",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/writedocs/writedocs.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/writedocs/writedocs/issues"
36
+ },
37
+ "author": "Gabriel Raeder",
38
+ "license": "ISC",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@apidevtools/swagger-parser": "^12.1.0",
44
+ "@astrojs/markdown-remark": "7.2.2",
45
+ "@astrojs/mdx": "^7.0.5",
46
+ "@astrojs/react": "^6.0.2",
47
+ "@astrojs/sitemap": "^3.7.3",
48
+ "@iconify-json/bi": "^1.2.7",
49
+ "@iconify-json/fa6-brands": "^1.2.6",
50
+ "@iconify-json/fa6-solid": "^1.2.4",
51
+ "@iconify-json/heroicons": "^1.2.3",
52
+ "@iconify-json/ion": "^1.2.7",
53
+ "@iconify-json/lucide": "^1.2.116",
54
+ "@iconify-json/mdi": "^1.2.3",
55
+ "@iconify-json/ri": "^1.2.10",
56
+ "@iconify-json/simple-icons": "^1.2.89",
57
+ "@iconify-json/tabler": "^1.2.35",
58
+ "@shikijs/transformers": "^4.3.1",
59
+ "@tailwindcss/vite": "^4.3.2",
60
+ "astro": "^7.2.2",
61
+ "astro-icon": "^1.1.5",
62
+ "commander": "^15.0.0",
63
+ "dotenv": "^17.4.2",
64
+ "gray-matter": "^4.0.3",
65
+ "katex": "^0.16.47",
66
+ "mermaid": "^11.16.0",
67
+ "pagefind": "^1.5.2",
68
+ "react": "^19.2.8",
69
+ "react-dom": "^19.2.8",
70
+ "rehype-katex": "^7.0.1",
71
+ "remark-math": "^6.0.0",
72
+ "shiki": "^4.3.1",
73
+ "tailwindcss": "^4.3.2",
74
+ "zod": "^4.4.3"
75
+ },
76
+ "engines": {
77
+ "node": ">=20.3.0"
78
+ }
79
+ }
Binary file
Binary file