@readme/cli 0.0.26

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 (144) hide show
  1. package/README.md +55 -0
  2. package/bin/readme.js +8 -0
  3. package/package.json +58 -0
  4. package/src/bootstrap.js +97 -0
  5. package/src/cli.js +189 -0
  6. package/src/commands/dev.js +119 -0
  7. package/src/commands/eyes.js +37 -0
  8. package/src/commands/import.js +2565 -0
  9. package/src/commands/lint.js +70 -0
  10. package/src/commands/oas-sync.js +364 -0
  11. package/src/commands/oas-validate.js +208 -0
  12. package/src/commands/play.js +17 -0
  13. package/src/commands/pretty.js +133 -0
  14. package/src/commands/setup.js +256 -0
  15. package/src/commands/versions.js +81 -0
  16. package/src/dev/.next/app-build-manifest.json +20 -0
  17. package/src/dev/.next/build-manifest.json +31 -0
  18. package/src/dev/.next/cache/.rscinfo +1 -0
  19. package/src/dev/.next/cache/next-devtools-config.json +1 -0
  20. package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
  21. package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
  22. package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
  23. package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
  24. package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
  25. package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
  26. package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
  27. package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
  28. package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
  29. package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
  30. package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
  31. package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
  32. package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
  33. package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
  34. package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
  35. package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
  36. package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
  37. package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
  38. package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
  39. package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
  40. package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
  41. package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
  42. package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
  43. package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
  44. package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
  45. package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
  46. package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
  47. package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
  48. package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
  49. package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
  50. package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
  51. package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
  52. package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
  53. package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
  54. package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
  55. package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
  56. package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
  57. package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
  58. package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
  59. package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
  60. package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
  61. package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
  62. package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
  63. package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
  64. package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
  65. package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
  66. package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
  67. package/src/dev/.next/package.json +1 -0
  68. package/src/dev/.next/prerender-manifest.json +11 -0
  69. package/src/dev/.next/react-loadable-manifest.json +1 -0
  70. package/src/dev/.next/routes-manifest.json +1 -0
  71. package/src/dev/.next/server/app/[...slug]/page.js +360 -0
  72. package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +1 -0
  73. package/src/dev/.next/server/app/page.js +349 -0
  74. package/src/dev/.next/server/app/page_client-reference-manifest.js +1 -0
  75. package/src/dev/.next/server/app-paths-manifest.json +3 -0
  76. package/src/dev/.next/server/edge-runtime-webpack.js +1151 -0
  77. package/src/dev/.next/server/interception-route-rewrite-manifest.js +1 -0
  78. package/src/dev/.next/server/middleware-build-manifest.js +33 -0
  79. package/src/dev/.next/server/middleware-manifest.json +32 -0
  80. package/src/dev/.next/server/middleware-react-loadable-manifest.js +1 -0
  81. package/src/dev/.next/server/middleware.js +1113 -0
  82. package/src/dev/.next/server/next-font-manifest.js +1 -0
  83. package/src/dev/.next/server/next-font-manifest.json +1 -0
  84. package/src/dev/.next/server/pages-manifest.json +5 -0
  85. package/src/dev/.next/server/server-reference-manifest.js +1 -0
  86. package/src/dev/.next/server/server-reference-manifest.json +5 -0
  87. package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +1 -0
  88. package/src/dev/.next/server/vendor-chunks/@readme.js +25 -0
  89. package/src/dev/.next/server/vendor-chunks/@swc.js +55 -0
  90. package/src/dev/.next/server/vendor-chunks/next.js +3659 -0
  91. package/src/dev/.next/server/webpack-runtime.js +209 -0
  92. package/src/dev/.next/static/chunks/app/[...slug]/loading.js +28 -0
  93. package/src/dev/.next/static/chunks/app/[...slug]/page.js +28 -0
  94. package/src/dev/.next/static/chunks/app/layout.js +171 -0
  95. package/src/dev/.next/static/chunks/app/page.js +28 -0
  96. package/src/dev/.next/static/chunks/app-pages-internals.js +182 -0
  97. package/src/dev/.next/static/chunks/main-app.js +1882 -0
  98. package/src/dev/.next/static/chunks/polyfills.js +1 -0
  99. package/src/dev/.next/static/chunks/webpack.js +1393 -0
  100. package/src/dev/.next/static/css/app/layout.css +559 -0
  101. package/src/dev/.next/static/development/_buildManifest.js +1 -0
  102. package/src/dev/.next/static/development/_ssgManifest.js +1 -0
  103. package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
  104. package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +1 -0
  105. package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +12 -0
  106. package/src/dev/.next/trace +21 -0
  107. package/src/dev/.next/types/app/[...slug]/page.ts +84 -0
  108. package/src/dev/.next/types/app/layout.ts +84 -0
  109. package/src/dev/.next/types/app/page.ts +84 -0
  110. package/src/dev/.next/types/cache-life.d.ts +141 -0
  111. package/src/dev/.next/types/package.json +1 -0
  112. package/src/dev/.next/types/routes.d.ts +55 -0
  113. package/src/dev/app/Sidebar.js +149 -0
  114. package/src/dev/app/[...slug]/loading.js +16 -0
  115. package/src/dev/app/[...slug]/page.js +43 -0
  116. package/src/dev/app/globals.css +167 -0
  117. package/src/dev/app/layout.js +73 -0
  118. package/src/dev/app/page.js +19 -0
  119. package/src/dev/lib/docs.js +337 -0
  120. package/src/dev/middleware.js +7 -0
  121. package/src/dev/next.config.mjs +22 -0
  122. package/src/index.js +12 -0
  123. package/src/prompts/index.js +352 -0
  124. package/src/utils/claude.js +15 -0
  125. package/src/utils/eyes.js +365 -0
  126. package/src/utils/git.js +143 -0
  127. package/src/utils/lint.js +99 -0
  128. package/src/utils/reporter.js +319 -0
  129. package/src/utils/setup-templates.js +323 -0
  130. package/src/utils/styles.js +50 -0
  131. package/src/utils/tamagotchi.js +1139 -0
  132. package/src/utils/tips.js +90 -0
  133. package/src/validators/components.js +230 -0
  134. package/src/validators/content.js +53 -0
  135. package/src/validators/duplicates.js +45 -0
  136. package/src/validators/frontmatter.js +247 -0
  137. package/src/validators/links.js +68 -0
  138. package/src/validators/nesting.js +50 -0
  139. package/src/validators/numbering.js +136 -0
  140. package/src/validators/oas-reference.js +126 -0
  141. package/src/validators/oas-schema.js +106 -0
  142. package/src/validators/ordering.js +121 -0
  143. package/src/validators/recipes.js +143 -0
  144. package/vendor/TOOLS.md +19 -0
@@ -0,0 +1,167 @@
1
+ :root {
2
+ /* @readme/markdown component color variables */
3
+ --blue-rgb: 1, 142, 245;
4
+ --yellow-rgb: 245, 174, 0;
5
+ --green-rgb: 0, 175, 80;
6
+ --red-rgb: 235, 55, 35;
7
+ --color-border-default: rgba(0, 0, 0, 0.06);
8
+ --color-text-default: #1a1a2e;
9
+ --color-text-muted: #4f5a66;
10
+ --color-text-minimum: #637288;
11
+ --markdown-radius: 8px;
12
+ --md-code-background: #f8fafc;
13
+ --md-code-text: #1e293b;
14
+ --md-code-radius: 8px;
15
+ --border-radius-lg: 8px;
16
+ --markdown-text: #374151;
17
+ --markdown-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
+ --markdown-line-height: 1.75;
19
+ --markdown-font-size: 15.5px;
20
+ --markdown-title: #0f172a;
21
+ --markdown-title-font: inherit;
22
+ --markdown-edge: #e2e8f0;
23
+ }
24
+
25
+ /*
26
+ * Content area prose styling.
27
+ * This targets rendered markdown injected via dangerouslySetInnerHTML
28
+ * where we can't apply Tailwind utility classes directly.
29
+ */
30
+
31
+ .content h1 {
32
+ font-size: 2rem;
33
+ font-weight: 800;
34
+ letter-spacing: -0.025em;
35
+ line-height: 1.2;
36
+ color: #0f172a;
37
+ margin-bottom: 0.5rem;
38
+ }
39
+
40
+ .excerpt {
41
+ font-size: 1.05rem;
42
+ color: #64748b;
43
+ line-height: 1.6;
44
+ margin-bottom: 2rem;
45
+ }
46
+
47
+ .content h2 {
48
+ font-size: 1.45rem;
49
+ font-weight: 700;
50
+ color: #0f172a;
51
+ letter-spacing: -0.01em;
52
+ margin: 2.5rem 0 0.75rem;
53
+ padding-bottom: 0.5rem;
54
+ border-bottom: 1px solid #f1f5f9;
55
+ }
56
+
57
+ .content h3 {
58
+ font-size: 1.15rem;
59
+ font-weight: 600;
60
+ color: #1e293b;
61
+ margin: 2rem 0 0.5rem;
62
+ }
63
+
64
+ .content p {
65
+ font-size: 15.5px;
66
+ line-height: 1.75;
67
+ color: #374151;
68
+ margin-bottom: 1.25rem;
69
+ }
70
+
71
+ .content ul,
72
+ .content ol {
73
+ margin: 0 0 1.25rem 1.5rem;
74
+ line-height: 1.75;
75
+ color: #374151;
76
+ }
77
+
78
+ .content li {
79
+ margin-bottom: 0.25rem;
80
+ }
81
+
82
+ .content code {
83
+ font-family: 'SF Mono', 'Fira Code', Menlo, Consolas, monospace;
84
+ font-size: 13px;
85
+ background: #f1f5f9;
86
+ color: #0f172a;
87
+ padding: 0.15em 0.4em;
88
+ border-radius: 5px;
89
+ font-weight: 500;
90
+ }
91
+
92
+ .content pre {
93
+ background: #0f172a;
94
+ color: #e2e8f0;
95
+ padding: 1.25rem 1.5rem;
96
+ border-radius: 12px;
97
+ overflow-x: auto;
98
+ margin-bottom: 1.5rem;
99
+ font-size: 13.5px;
100
+ line-height: 1.65;
101
+ border: 1px solid rgba(255, 255, 255, 0.06);
102
+ }
103
+
104
+ .content pre code {
105
+ background: none;
106
+ padding: 0;
107
+ color: inherit;
108
+ font-size: inherit;
109
+ font-weight: normal;
110
+ border-radius: 0;
111
+ }
112
+
113
+ .content a {
114
+ color: #018ef5;
115
+ text-decoration: none;
116
+ font-weight: 500;
117
+ }
118
+
119
+ .content a:hover {
120
+ text-decoration: underline;
121
+ text-underline-offset: 2px;
122
+ }
123
+
124
+ .content blockquote {
125
+ border-left: 3px solid #018ef5;
126
+ padding: 0.75rem 1.25rem;
127
+ margin: 0 0 1.5rem;
128
+ color: #475569;
129
+ background: #f8fafc;
130
+ border-radius: 0 10px 10px 0;
131
+ font-size: 15px;
132
+ }
133
+
134
+ .content table {
135
+ width: 100%;
136
+ border-collapse: collapse;
137
+ margin-bottom: 1.5rem;
138
+ font-size: 14.5px;
139
+ }
140
+
141
+ .content th,
142
+ .content td {
143
+ border: 1px solid #e2e8f0;
144
+ padding: 0.6rem 1rem;
145
+ text-align: left;
146
+ }
147
+
148
+ .content th {
149
+ background: #f8fafc;
150
+ font-weight: 600;
151
+ color: #1e293b;
152
+ font-size: 13px;
153
+ text-transform: uppercase;
154
+ letter-spacing: 0.03em;
155
+ }
156
+
157
+ .content img {
158
+ max-width: 100%;
159
+ border-radius: 12px;
160
+ margin: 0.5rem 0;
161
+ }
162
+
163
+ .content hr {
164
+ border: none;
165
+ border-top: 1px solid #f1f5f9;
166
+ margin: 2rem 0;
167
+ }
@@ -0,0 +1,73 @@
1
+ import { collectSidebar } from '../lib/docs';
2
+ import { SidebarNav, SidebarPanel } from './Sidebar';
3
+ import '@readme/markdown/dist/main.css';
4
+ import './globals.css';
5
+
6
+ export const metadata = {
7
+ title: {
8
+ default: 'Dev Preview',
9
+ template: '%s | Dev Preview',
10
+ },
11
+ };
12
+
13
+ export default async function RootLayout({ children }) {
14
+ const sidebar = collectSidebar();
15
+
16
+ return (
17
+ <html lang="en">
18
+ <head>
19
+ <script src="https://cdn.tailwindcss.com"></script>
20
+ <link rel="stylesheet" href="https://kit.fontawesome.com/b331e91c9c.css" crossOrigin="anonymous" />
21
+ </head>
22
+ <body className="bg-[#000343] text-gray-900 antialiased">
23
+ {/* Top Navigation */}
24
+ <header className="fixed top-0 inset-x-0 h-[48px] bg-[#000343] flex items-center px-5 z-50" style={{
25
+ backgroundImage: `linear-gradient(rgba(255,255,255,0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.08) 1px, transparent 1px)`,
26
+ backgroundSize: '20px 20px',
27
+ }}>
28
+ <div className="flex items-center gap-3 mr-8 shrink-0">
29
+ <div className="flex items-center gap-2">
30
+ <i className="fa-brands fa-readme text-[#018ef5] text-[18px] relative top-[1px]" />
31
+ <span className="text-white/90 font-mono text-[13px] tracking-tight mr-1">dev server</span>
32
+ </div>
33
+ <span className="relative group flex items-center">
34
+ <span className="text-[8px] font-semibold bg-white/20 text-white/50 group-hover:bg-white/30 group-hover:text-white/70 px-1.5 py-[3px] rounded uppercase tracking-wider cursor-default transition-colors duration-150">
35
+ beta
36
+ </span>
37
+ <span className="pointer-events-none absolute left-1/2 -translate-x-1/2 top-full mt-5 z-[100] w-48 rounded-lg bg-gray-900 px-3 py-2 text-[11px] leading-snug text-white/80 opacity-0 group-hover:opacity-100 transition-opacity duration-150 shadow-lg">
38
+ This is a local preview — it won't look exactly like your published docs.
39
+ </span>
40
+ </span>
41
+ </div>
42
+ <SidebarNav sidebar={sidebar} />
43
+ </header>
44
+
45
+ <div className="flex pt-[48px] min-h-screen">
46
+ {/* Sidebar */}
47
+ <aside className="w-[300px] fixed top-[48px] left-0 bottom-0 bg-white rounded-tl-xl border-r border-gray-200 overflow-y-auto py-4 pl-5">
48
+ <SidebarPanel sidebar={sidebar} />
49
+ </aside>
50
+
51
+ {/* Content */}
52
+ <main className="ml-[300px] flex-1 min-w-0 bg-white rounded-tr-xl">
53
+ <div className="max-w-[820px] py-10 px-16">
54
+ {children}
55
+ </div>
56
+ </main>
57
+ </div>
58
+
59
+ <style dangerouslySetInnerHTML={{ __html: `
60
+ [data-sidebar-group][data-expanded="false"] .sidebar-children { display: none; }
61
+ [data-sidebar-group][data-expanded="true"] .sidebar-children { display: block; }
62
+ [data-sidebar-group][data-expanded="true"] .sidebar-chevron { transform: rotate(90deg); }
63
+ `}} />
64
+ <script
65
+ dangerouslySetInnerHTML={{
66
+ __html: `(function(){var es=new EventSource('/__reload');es.onmessage=function(e){if(e.data==='reload')location.reload()};})();
67
+ document.addEventListener('click',function(e){var t=e.target.closest('[data-sidebar-toggle]');if(!t)return;var g=t.closest('[data-sidebar-group]');if(!g)return;if(t.getAttribute('href')==='#'){e.preventDefault();}var ex=g.getAttribute('data-expanded')==='true';g.setAttribute('data-expanded',ex?'false':'true');});`,
68
+ }}
69
+ />
70
+ </body>
71
+ </html>
72
+ );
73
+ }
@@ -0,0 +1,19 @@
1
+ import { redirect } from 'next/navigation';
2
+ import { getFirstPageHref } from '../lib/docs';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export default function HomePage() {
7
+ const firstHref = getFirstPageHref();
8
+ if (firstHref) {
9
+ redirect(firstHref);
10
+ }
11
+
12
+ return (
13
+ <div className="flex flex-col items-center justify-center py-24 text-center">
14
+ <div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-2xl mb-4">📝</div>
15
+ <h1 className="text-xl font-semibold text-gray-900 mb-1">Welcome</h1>
16
+ <p className="text-sm text-gray-400">No documentation pages found.</p>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,337 @@
1
+ import { createRequire } from 'node:module';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import matter from 'gray-matter';
5
+ import { compile, run } from '@readme/markdown';
6
+ import yaml from 'js-yaml';
7
+
8
+ // Use createRequire to load react-dom/server at runtime,
9
+ // bypassing Next.js App Router's static import restriction.
10
+ const _require = createRequire(import.meta.url);
11
+ const React = _require('react');
12
+ const { renderToStaticMarkup } = _require('react-dom/server');
13
+
14
+ const DOCS_ROOT = process.env.DOCS_ROOT;
15
+
16
+ const SECTIONS = [
17
+ { dir: 'docs', label: 'Docs' },
18
+ { dir: 'reference', label: 'Reference' },
19
+ { dir: 'custom_pages', label: 'Custom Pages' },
20
+ { dir: 'recipes', label: 'Recipes' },
21
+ ];
22
+
23
+ // Max folder nesting per section — anything deeper doesn't render in ReadMe's
24
+ // sidebar, so we mirror the cap here. Keep in sync with src/validators/nesting.js.
25
+ const MAX_DEPTH = {
26
+ docs: 3,
27
+ reference: 3,
28
+ recipes: 0,
29
+ custom_pages: 0,
30
+ };
31
+
32
+ function walkDir(dirPath, sectionDir, depth = 0) {
33
+ if (!fs.existsSync(dirPath)) return [];
34
+
35
+ // Read _order.yaml for ordering
36
+ let order = [];
37
+ const orderPath = path.join(dirPath, '_order.yaml');
38
+ if (fs.existsSync(orderPath)) {
39
+ order = yaml.load(fs.readFileSync(orderPath, 'utf-8')) || [];
40
+ }
41
+
42
+ // Get all items (exclude dotfiles and _-prefixed files)
43
+ const items = fs.readdirSync(dirPath).filter(f => !f.startsWith('.') && !f.startsWith('_'));
44
+
45
+ const mdFiles = new Set(items.filter(f => f.endsWith('.md') && f !== 'index.md').map(f => f.replace(/\.md$/, '')));
46
+ const dirs = new Set(
47
+ items.filter(f => {
48
+ try {
49
+ return fs.statSync(path.join(dirPath, f)).isDirectory();
50
+ } catch {
51
+ return false;
52
+ }
53
+ }),
54
+ );
55
+
56
+ // Build ordered list: ordered items first, then any remaining
57
+ const allNames = new Set([...mdFiles, ...dirs]);
58
+ const ordered = order.filter(name => allNames.has(name));
59
+ const remaining = [...allNames].filter(name => !ordered.includes(name));
60
+ const finalOrder = [...ordered, ...remaining];
61
+
62
+ const entries = [];
63
+ for (const name of finalOrder) {
64
+ const isDir = dirs.has(name);
65
+ const isMd = mdFiles.has(name);
66
+
67
+ if (isDir) {
68
+ // Skip folders whose contents would exceed ReadMe's sidebar nesting
69
+ // cap — they won't render in production, so don't show them here either.
70
+ const maxDepth = MAX_DEPTH[sectionDir];
71
+ if (maxDepth !== undefined && depth + 1 > maxDepth) continue;
72
+
73
+ const children = walkDir(path.join(dirPath, name), sectionDir, depth + 1);
74
+
75
+ // ReadMeConfig pages are internal — show them without a group header
76
+ if (name === 'ReadMeConfig') {
77
+ entries.push(...children);
78
+ continue;
79
+ }
80
+
81
+ // A directory can represent either:
82
+ // 1. A pure category (no content of its own) → render as group label
83
+ // 2. A parent page with children → render as a clickable page with
84
+ // a nested list underneath
85
+ //
86
+ // Two file layouts signal #2:
87
+ // a. `<name>/index.md` — the "index" convention
88
+ // b. `<parent>/<name>.md` right alongside the `<name>/` folder
89
+ // (sibling convention) — the importer writes parent pages this way
90
+ // If either exists, treat this as a clickable parent page.
91
+ const indexPath = path.join(dirPath, name, 'index.md');
92
+ const siblingMdPath = path.join(dirPath, `${name}.md`);
93
+ const parentPagePath = fs.existsSync(indexPath)
94
+ ? indexPath
95
+ : fs.existsSync(siblingMdPath) ? siblingMdPath : null;
96
+
97
+ let title = name;
98
+ let href = null;
99
+ let hidden = false;
100
+ let icon = null;
101
+ if (parentPagePath) {
102
+ const { data } = matter(fs.readFileSync(parentPagePath, 'utf-8'));
103
+ title = data.title || name;
104
+ href = `/${sectionDir}/${encodeURIComponent(name)}`;
105
+ hidden = !!data.hidden;
106
+ icon = data.icon || null;
107
+ }
108
+
109
+ entries.push({ title, href, hidden, icon, children });
110
+ // If a sibling .md is providing the parent content, remove it from
111
+ // mdFiles so we don't also render it as a standalone page below.
112
+ mdFiles.delete(name);
113
+ } else if (isMd) {
114
+ const filePath = path.join(dirPath, `${name}.md`);
115
+ const { data } = matter(fs.readFileSync(filePath, 'utf-8'));
116
+ const linkUrl = data.link?.url;
117
+ entries.push({
118
+ title: data.title || name,
119
+ href: linkUrl || `/${sectionDir}/${encodeURIComponent(name)}`,
120
+ external: !!linkUrl,
121
+ hidden: !!data.hidden,
122
+ icon: data.icon || null,
123
+ children: [],
124
+ });
125
+ }
126
+ }
127
+
128
+ return entries;
129
+ }
130
+
131
+ export function collectSidebar() {
132
+ const sections = [];
133
+ for (const { dir, label } of SECTIONS) {
134
+ const fullPath = path.join(DOCS_ROOT, dir);
135
+ if (!fs.existsSync(fullPath)) continue;
136
+
137
+ const children = walkDir(fullPath, dir);
138
+ if (children.length > 0) {
139
+ sections.push({ title: label, dir, firstHref: findFirstHref(children), children });
140
+ }
141
+ }
142
+ return sections;
143
+ }
144
+
145
+ function loadCustomBlocks(docsRoot) {
146
+ const blocksDir = path.join(docsRoot, 'custom_blocks');
147
+ if (!fs.existsSync(blocksDir)) return { sources: {}, modules: {} };
148
+
149
+ const files = fs.readdirSync(blocksDir).filter(f => f.endsWith('.md') || f.endsWith('.mdx'));
150
+ const sources = {};
151
+ const modules = {};
152
+
153
+ for (const file of files) {
154
+ const raw = fs.readFileSync(path.join(blocksDir, file), 'utf-8');
155
+ const { data, content } = matter(raw);
156
+ const name = data.name || path.basename(file, path.extname(file));
157
+ sources[name] = content;
158
+ modules[name] = run(compile(content));
159
+ }
160
+
161
+ return { sources, modules };
162
+ }
163
+
164
+ // Recursively find a .md file matching the given slug within a directory
165
+ function findFile(dirPath, slug) {
166
+ if (!fs.existsSync(dirPath)) return null;
167
+
168
+ // Check for slug.md directly in this directory
169
+ const direct = path.join(dirPath, `${slug}.md`);
170
+ if (fs.existsSync(direct)) return direct;
171
+
172
+ // Check for slug/index.md
173
+ const index = path.join(dirPath, slug, 'index.md');
174
+ if (fs.existsSync(index)) return index;
175
+
176
+ // Search subdirectories
177
+ const entries = fs.readdirSync(dirPath).filter(f => !f.startsWith('.') && !f.startsWith('_'));
178
+ for (const entry of entries) {
179
+ const full = path.join(dirPath, entry);
180
+ try {
181
+ if (fs.statSync(full).isDirectory()) {
182
+ const found = findFile(full, slug);
183
+ if (found) return found;
184
+ }
185
+ } catch {
186
+ // skip
187
+ }
188
+ }
189
+
190
+ return null;
191
+ }
192
+
193
+ export function getPage(slugArray) {
194
+ // slugArray = ['docs', 'getting-started'] or ['reference', 'getPet']
195
+ const [section, ...rest] = slugArray.map(s => decodeURIComponent(s));
196
+ const slug = rest.join('/');
197
+ if (!section || !slug) return null;
198
+
199
+ const sectionPath = path.join(DOCS_ROOT, section);
200
+ const filePath = findFile(sectionPath, slug);
201
+
202
+ if (!filePath) return null;
203
+
204
+ const raw = fs.readFileSync(filePath, 'utf-8');
205
+ const { data, content } = matter(raw);
206
+
207
+ let html;
208
+
209
+ // Reference pages stubbed by the import command (or by oas:sync) have an
210
+ // empty body — the real endpoint UI comes from the OAS spec at render
211
+ // time. The production ReadMe renderer handles this; our local dev server
212
+ // used to show a blank page. If we see `api.file` + `api.operationId`,
213
+ // render a minimal preview from the spec so the page isn't empty.
214
+ if (data.api?.file && data.api?.operationId && !content.trim()) {
215
+ html = renderOasOperationPreview(data.api.file, data.api.operationId);
216
+ } else {
217
+ try {
218
+ const { sources, modules } = loadCustomBlocks(DOCS_ROOT);
219
+ const compiled = compile(content, { components: sources });
220
+ const mod = run(compiled, { components: modules });
221
+
222
+ // Suppress React warnings about class vs className from raw HTML in markdown
223
+ const origError = console.error;
224
+ console.error = (...args) => {
225
+ if (typeof args[0] === 'string' && args[0].includes('Invalid DOM property')) return;
226
+ origError.apply(console, args);
227
+ };
228
+ html = renderToStaticMarkup(React.createElement(mod.default));
229
+ console.error = origError;
230
+ } catch {
231
+ html = `<p><em>Error rendering MDX. Raw content below:</em></p><pre>${content.replace(/</g, '&lt;')}</pre>`;
232
+ }
233
+ }
234
+
235
+ return {
236
+ title: data.title || slug,
237
+ excerpt: data.excerpt || '',
238
+ html,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Render a small HTML preview of one OAS operation — method badge, path,
244
+ * description, and parameter list. Not a full interactive API reference
245
+ * (production ReadMe handles that); just enough so dev server pages show
246
+ * something meaningful instead of a blank body.
247
+ */
248
+ function renderOasOperationPreview(specFile, operationId) {
249
+ const specPath = path.join(DOCS_ROOT, 'reference', specFile);
250
+ if (!fs.existsSync(specPath)) {
251
+ return `<p><em>OAS spec not found: <code>reference/${specFile}</code></em></p>`;
252
+ }
253
+
254
+ let spec;
255
+ try {
256
+ const rawSpec = fs.readFileSync(specPath, 'utf-8');
257
+ spec = specFile.toLowerCase().endsWith('.json') ? JSON.parse(rawSpec) : yaml.load(rawSpec);
258
+ } catch (e) {
259
+ return `<p><em>Couldn't parse <code>reference/${specFile}</code>: ${escapeHtml(e.message)}</em></p>`;
260
+ }
261
+
262
+ let found = null;
263
+ for (const [pathKey, pathItem] of Object.entries(spec.paths || {})) {
264
+ for (const [method, op] of Object.entries(pathItem || {})) {
265
+ if (op && typeof op === 'object' && op.operationId === operationId) {
266
+ found = { method: method.toUpperCase(), path: pathKey, op };
267
+ }
268
+ }
269
+ }
270
+ if (!found) {
271
+ return `<p><em>Operation <code>${escapeHtml(operationId)}</code> not found in <code>${escapeHtml(specFile)}</code>.</em></p>`;
272
+ }
273
+
274
+ const { method, path: opPath, op } = found;
275
+ const methodColor = {
276
+ GET: '#22c55e', POST: '#3b82f6', PUT: '#f59e0b', PATCH: '#a855f7', DELETE: '#ef4444',
277
+ }[method] || '#6b7280';
278
+
279
+ const parts = [];
280
+ parts.push(
281
+ `<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem;">` +
282
+ `<span style="background: ${methodColor}; color: white; padding: 0.15rem 0.5rem; border-radius: 0.25rem; font-family: monospace; font-weight: 600; font-size: 0.85rem;">${method}</span>` +
283
+ `<code style="font-size: 1rem;">${escapeHtml(opPath)}</code>` +
284
+ `</div>`,
285
+ );
286
+ if (op.description) parts.push(`<p>${escapeHtml(op.description)}</p>`);
287
+
288
+ if (Array.isArray(op.parameters) && op.parameters.length > 0) {
289
+ parts.push(`<h2>Parameters</h2>`);
290
+ parts.push(`<ul>`);
291
+ for (const p of op.parameters) {
292
+ const name = p.name || '(unnamed)';
293
+ const where = p.in ? ` <code style="color: #6b7280;">(${p.in})</code>` : '';
294
+ const required = p.required ? ' <strong>required</strong>' : '';
295
+ const desc = p.description ? ` — ${escapeHtml(p.description)}` : '';
296
+ parts.push(`<li><code>${escapeHtml(name)}</code>${where}${required}${desc}</li>`);
297
+ }
298
+ parts.push(`</ul>`);
299
+ }
300
+
301
+ if (op.responses && typeof op.responses === 'object') {
302
+ parts.push(`<h2>Responses</h2>`);
303
+ parts.push(`<ul>`);
304
+ for (const [status, resp] of Object.entries(op.responses)) {
305
+ const desc = resp?.description ? ` — ${escapeHtml(resp.description)}` : '';
306
+ parts.push(`<li><code>${escapeHtml(status)}</code>${desc}</li>`);
307
+ }
308
+ parts.push(`</ul>`);
309
+ }
310
+
311
+ parts.push(
312
+ `<hr style="margin: 2rem 0; border: 0; border-top: 1px solid #e5e7eb;">`,
313
+ `<p style="color: #6b7280; font-size: 0.875rem;"><em>Local preview rendered from the OAS spec. Full interactive API reference (try-it-out, request/response schemas, auth) renders when imported into ReadMe.</em></p>`,
314
+ );
315
+
316
+ return parts.join('\n');
317
+ }
318
+
319
+ function escapeHtml(s) {
320
+ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
321
+ }
322
+
323
+ function findFirstHref(items) {
324
+ for (const item of items) {
325
+ if (item.href) return item.href;
326
+ if (item.children?.length) {
327
+ const found = findFirstHref(item.children);
328
+ if (found) return found;
329
+ }
330
+ }
331
+ return null;
332
+ }
333
+
334
+ export function getFirstPageHref() {
335
+ const sidebar = collectSidebar();
336
+ return findFirstHref(sidebar);
337
+ }
@@ -0,0 +1,7 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export function middleware(request) {
4
+ const response = NextResponse.next();
5
+ response.headers.set('x-pathname', request.nextUrl.pathname);
6
+ return response;
7
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+
6
+ /** @type {import('next').NextConfig} */
7
+ export default {
8
+ outputFileTracingRoot: __dirname,
9
+ // When installed via `npx`, this directory lives inside `node_modules`, and
10
+ // Next's SWC loader excludes `node_modules` from JSX transformation by
11
+ // default. Marking our own package as a transpile target bypasses that.
12
+ transpilePackages: ['@readme/cli'],
13
+ serverExternalPackages: ['gray-matter', 'js-yaml', '@readme/markdown'],
14
+ webpack: (config) => {
15
+ // Suppress noisy cache serialization warnings from large @readme/markdown bundle
16
+ config.infrastructureLogging = {
17
+ ...config.infrastructureLogging,
18
+ level: 'error',
19
+ };
20
+ return config;
21
+ },
22
+ };
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Programmatic API for `@readme/cli`.
3
+ *
4
+ * Each function mirrors the behavior of its CLI command but returns
5
+ * structured data and never calls `process.exit`. Callers own how to format,
6
+ * report, and exit on errors.
7
+ */
8
+
9
+ export { lint } from './commands/lint.js';
10
+ export { syncOas } from './commands/oas-sync.js';
11
+ export { validateOas } from './commands/oas-validate.js';
12
+ export { importDocs } from './commands/import.js';