doxla 0.3.8 → 0.4.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.
- package/README.md +6 -5
- package/dist/cli/index.js +6 -5
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -1
- package/src/app/package.json +1 -0
- package/src/app/src/components/DocPage.tsx +12 -2
- package/src/app/src/components/MarkdownRenderer.tsx +1 -23
- package/src/app/src/components/MdxRenderer.tsx +124 -0
- package/src/app/src/components/mdx/Callout.tsx +28 -0
- package/src/app/src/components/mdx/MdxErrorBoundary.tsx +44 -0
- package/src/app/src/lib/doc-links.ts +28 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# doxla
|
|
2
2
|
|
|
3
|
-
Improve documentation discoverability within repos. Doxla discovers all `.md` files in your repository, builds a beautiful docs viewer, and deploys it to GitHub Pages.
|
|
3
|
+
Improve documentation discoverability within repos. Doxla discovers all `.md` and `.mdx` files in your repository, builds a beautiful docs viewer, and deploys it to GitHub Pages.
|
|
4
4
|
|
|
5
5
|
## Why?
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Documentation is most valuable when it lives next to the code it describes — b
|
|
|
8
8
|
|
|
9
9
|
Meanwhile, AI coding assistants (Claude Code, Copilot, Cursor) work with what's in the repository. Docs in Notion or Google Docs are invisible to them. In-repo markdown is context they can read and act on.
|
|
10
10
|
|
|
11
|
-
Doxla bridges the gap: keep your docs as `.md` files in your repo (where both humans and AI agents can find them), and Doxla turns them into a readable, searchable site.
|
|
11
|
+
Doxla bridges the gap: keep your docs as `.md` or `.mdx` files in your repo (where both humans and AI agents can find them), and Doxla turns them into a readable, searchable site.
|
|
12
12
|
|
|
13
13
|
Read the full rationale: [Why Doxla?](RATIONALE.md)
|
|
14
14
|
|
|
@@ -36,11 +36,12 @@ This creates a GitHub Actions workflow that builds and deploys your docs on ever
|
|
|
36
36
|
npx doxla build
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
This discovers all markdown files, builds a static docs site, and outputs it to `doxla-dist/`.
|
|
39
|
+
This discovers all markdown and MDX files, builds a static docs site, and outputs it to `doxla-dist/`.
|
|
40
40
|
|
|
41
41
|
## Features
|
|
42
42
|
|
|
43
|
-
- Automatic
|
|
43
|
+
- Automatic `.md` and `.mdx` file discovery (respects `.gitignore`-style patterns)
|
|
44
|
+
- MDX support with built-in components (`<Callout>`) and JSX expressions
|
|
44
45
|
- Beautiful React-based docs viewer with sidebar navigation
|
|
45
46
|
- Full-text search across all documents
|
|
46
47
|
- Syntax highlighting for code blocks
|
|
@@ -64,7 +65,7 @@ Creates `.github/workflows/doxla.yml` for automatic GitHub Pages deployment.
|
|
|
64
65
|
|
|
65
66
|
## How It Works
|
|
66
67
|
|
|
67
|
-
1. **Discover** - Scans your repo for `.md` files (excluding `node_modules`, `.git`, etc.)
|
|
68
|
+
1. **Discover** - Scans your repo for `.md` and `.mdx` files (excluding `node_modules`, `.git`, etc.)
|
|
68
69
|
2. **Manifest** - Reads each file, extracts titles, and generates a JSON manifest
|
|
69
70
|
3. **Build** - Copies the built-in React app template, injects the manifest, and runs `vite build`
|
|
70
71
|
4. **Output** - Produces a static site ready for any hosting
|
package/dist/cli/index.js
CHANGED
|
@@ -86,14 +86,14 @@ var IGNORE_PATTERNS = [
|
|
|
86
86
|
"**/coverage/**"
|
|
87
87
|
];
|
|
88
88
|
async function discoverMarkdownFiles(rootDir) {
|
|
89
|
-
const files = await fg("**/*.md", {
|
|
89
|
+
const files = await fg("**/*.{md,mdx}", {
|
|
90
90
|
cwd: rootDir,
|
|
91
91
|
ignore: IGNORE_PATTERNS,
|
|
92
92
|
onlyFiles: true
|
|
93
93
|
});
|
|
94
94
|
return files.sort((a, b) => {
|
|
95
|
-
const aIsReadme =
|
|
96
|
-
const bIsReadme =
|
|
95
|
+
const aIsReadme = /^readme\.mdx?$/i.test(a);
|
|
96
|
+
const bIsReadme = /^readme\.mdx?$/i.test(b);
|
|
97
97
|
if (aIsReadme && !bIsReadme) return -1;
|
|
98
98
|
if (!aIsReadme && bIsReadme) return 1;
|
|
99
99
|
return a.localeCompare(b);
|
|
@@ -108,7 +108,8 @@ function extractTitle(content, filePath) {
|
|
|
108
108
|
if (match) {
|
|
109
109
|
return match[1].trim();
|
|
110
110
|
}
|
|
111
|
-
const
|
|
111
|
+
const ext = filePath.endsWith(".mdx") ? ".mdx" : ".md";
|
|
112
|
+
const name = basename(filePath, ext);
|
|
112
113
|
if (name.toLowerCase() === "readme") {
|
|
113
114
|
const dir = dirname(filePath);
|
|
114
115
|
if (dir === ".") return "README";
|
|
@@ -117,7 +118,7 @@ function extractTitle(content, filePath) {
|
|
|
117
118
|
return name.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
118
119
|
}
|
|
119
120
|
function createSlug(filePath) {
|
|
120
|
-
return filePath.replace(/\.
|
|
121
|
+
return filePath.replace(/\.mdx?$/i, "").replace(/\\/g, "/").toLowerCase().replace(/[^a-z0-9/.-]/g, "-");
|
|
121
122
|
}
|
|
122
123
|
async function generateManifest(rootDir, files) {
|
|
123
124
|
const docs = await Promise.all(
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/init.ts","../../src/cli/templates/doxla-workflow.ts","../../src/cli/commands/build.ts","../../src/cli/lib/discover.ts","../../src/cli/lib/manifest.ts","../../src/cli/lib/build-app.ts","../../src/cli/lib/template.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { buildCommand } from \"./commands/build.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"doxla\")\n .description(\"Improve documentation discoverability within repos\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"init\")\n .description(\"Set up GitHub Actions workflow for Doxla docs deployment\")\n .action(initCommand);\n\nprogram\n .command(\"build\")\n .description(\"Discover markdown files and build the docs viewer\")\n .option(\"-o, --output <dir>\", \"Output directory\", \"doxla-dist\")\n .option(\"-r, --root <dir>\", \"Root directory to scan for markdown files\", \".\")\n .option(\"--base-path <path>\", \"Base path for GitHub Pages deployment\", \"/\")\n .action(buildCommand);\n\nprogram.parse();\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport chalk from \"chalk\";\nimport { workflowTemplate } from \"../templates/doxla-workflow.js\";\n\nexport async function initCommand() {\n const workflowDir = join(process.cwd(), \".github\", \"workflows\");\n const workflowPath = join(workflowDir, \"doxla.yml\");\n\n if (existsSync(workflowPath)) {\n console.log(\n chalk.yellow(\"⚠ Workflow file already exists at .github/workflows/doxla.yml\")\n );\n console.log(chalk.yellow(\" Overwriting with latest template...\"));\n }\n\n await mkdir(workflowDir, { recursive: true });\n await writeFile(workflowPath, workflowTemplate, \"utf-8\");\n\n console.log(chalk.green(\"✓ Created .github/workflows/doxla.yml\"));\n console.log();\n console.log(\"Next steps:\");\n console.log(\n ` 1. Enable GitHub Pages in your repo settings (Settings → Pages → Source: ${chalk.bold(\"GitHub Actions\")})`\n );\n console.log(\" 2. Commit and push the workflow file\");\n console.log(\n \" 3. Your docs will be built and deployed on every push to main\"\n );\n}\n","export const workflowTemplate = `name: Deploy Doxla Docs\n\non:\n push:\n branches: [main]\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\nconcurrency:\n group: \"pages\"\n cancel-in-progress: false\n\njobs:\n build-and-deploy:\n runs-on: ubuntu-latest\n environment:\n name: github-pages\n url: \\${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n - run: npx doxla@latest build --base-path \"/\\${{ github.event.repository.name }}\"\n - uses: actions/upload-pages-artifact@v3\n with:\n path: ./doxla-dist\n - id: deployment\n uses: actions/deploy-pages@v4\n`;\n","import { resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { discoverMarkdownFiles } from \"../lib/discover.js\";\nimport { generateManifest } from \"../lib/manifest.js\";\nimport { buildApp } from \"../lib/build-app.js\";\n\ninterface BuildOptions {\n output: string;\n root: string;\n basePath: string;\n}\n\nexport async function buildCommand(options: BuildOptions) {\n const rootDir = resolve(options.root);\n const outputDir = resolve(options.output);\n const basePath = options.basePath;\n\n console.log(chalk.bold(\"doxla build\"));\n console.log();\n\n // Step 1: Discover markdown files\n const discoverSpinner = ora(\"Discovering markdown files...\").start();\n const files = await discoverMarkdownFiles(rootDir);\n\n if (files.length === 0) {\n discoverSpinner.fail(\"No markdown files found\");\n process.exit(1);\n }\n\n discoverSpinner.succeed(`Found ${files.length} markdown file${files.length === 1 ? \"\" : \"s\"}`);\n\n // Step 2: Generate manifest\n const manifestSpinner = ora(\"Generating manifest...\").start();\n const manifest = await generateManifest(rootDir, files);\n manifestSpinner.succeed(\"Manifest generated\");\n\n // Step 3: Build docs viewer\n const buildSpinner = ora(\"Building docs viewer...\").start();\n try {\n await buildApp(manifest, { output: outputDir, basePath });\n buildSpinner.succeed(\"Docs viewer built\");\n } catch (error) {\n buildSpinner.fail(\"Build failed\");\n console.error(chalk.red((error as Error).message));\n process.exit(1);\n }\n\n console.log();\n console.log(chalk.green(`✓ Output written to ${options.output}/`));\n}\n","import fg from \"fast-glob\";\n\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/doxla-dist/**\",\n \"**/.next/**\",\n \"**/coverage/**\",\n];\n\nexport async function discoverMarkdownFiles(rootDir: string): Promise<string[]> {\n const files = await fg(\"**/*.md\", {\n cwd: rootDir,\n ignore: IGNORE_PATTERNS,\n onlyFiles: true,\n });\n\n // Sort: README.md first, then alphabetical\n return files.sort((a, b) => {\n const aIsReadme = a.toLowerCase() === \"readme.md\";\n const bIsReadme = b.toLowerCase() === \"readme.md\";\n if (aIsReadme && !bIsReadme) return -1;\n if (!aIsReadme && bIsReadme) return 1;\n return a.localeCompare(b);\n });\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join, basename, dirname } from \"node:path\";\nimport type { Manifest, DocFile } from \"../../app/src/types/manifest.js\";\n\nfunction extractTitle(content: string, filePath: string): string {\n // Try to extract from first # heading\n const match = content.match(/^#\\s+(.+)$/m);\n if (match) {\n return match[1].trim();\n }\n\n // Fallback to filename without extension\n const name = basename(filePath, \".md\");\n if (name.toLowerCase() === \"readme\") {\n const dir = dirname(filePath);\n if (dir === \".\") return \"README\";\n return `${dir} - README`;\n }\n return name.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction createSlug(filePath: string): string {\n return filePath\n .replace(/\\.md$/i, \"\")\n .replace(/\\\\/g, \"/\")\n .toLowerCase()\n .replace(/[^a-z0-9/.-]/g, \"-\");\n}\n\nexport async function generateManifest(\n rootDir: string,\n files: string[]\n): Promise<Manifest> {\n const docs: DocFile[] = await Promise.all(\n files.map(async (filePath) => {\n const fullPath = join(rootDir, filePath);\n const content = await readFile(fullPath, \"utf-8\");\n return {\n slug: createSlug(filePath),\n path: filePath,\n title: extractTitle(content, filePath),\n content,\n };\n })\n );\n\n const repoName = basename(rootDir);\n\n return {\n repoName,\n generatedAt: new Date().toISOString(),\n totalDocs: docs.length,\n docs,\n };\n}\n","import { cp, writeFile, rm, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport type { Manifest } from \"../../app/src/types/manifest.js\";\nimport { getTemplatePath } from \"./template.js\";\n\ninterface BuildOptions {\n output: string;\n basePath: string;\n}\n\nexport async function buildApp(\n manifest: Manifest,\n options: BuildOptions\n): Promise<void> {\n const templateDir = getTemplatePath();\n const tempDir = join(\n tmpdir(),\n `doxla-build-${randomBytes(6).toString(\"hex\")}`\n );\n\n try {\n // Copy template to temp dir\n await cp(templateDir, tempDir, { recursive: true });\n\n // Write manifest.json into the app's src directory\n await writeFile(\n join(tempDir, \"src\", \"manifest.json\"),\n JSON.stringify(manifest, null, 2),\n \"utf-8\"\n );\n\n // Install dependencies\n execSync(\"npm install --no-audit --no-fund\", {\n cwd: tempDir,\n stdio: \"pipe\",\n });\n\n // Build with Vite, passing base path as env var\n execSync(\"npx vite build\", {\n cwd: tempDir,\n stdio: \"pipe\",\n env: { ...process.env, VITE_BASE_PATH: options.basePath },\n });\n\n // Copy output\n await rm(options.output, { recursive: true, force: true });\n await mkdir(options.output, { recursive: true });\n await cp(join(tempDir, \"dist\"), options.output, { recursive: true });\n } finally {\n // Clean up temp dir\n await rm(tempDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nexport function getTemplatePath(): string {\n const __filename = fileURLToPath(import.meta.url);\n let dir = dirname(__filename);\n\n // Walk up from the current file location until we find src/app\n // Works both from source (src/cli/lib/) and built (dist/cli/)\n for (let i = 0; i < 5; i++) {\n const candidate = resolve(dir, \"src\", \"app\");\n if (existsSync(resolve(candidate, \"package.json\"))) {\n return candidate;\n }\n dir = dirname(dir);\n }\n\n // Fallback: assume built location dist/cli/ -> ../../src/app\n const fallback = resolve(dirname(__filename), \"..\", \"..\", \"src\", \"app\");\n return fallback;\n}\n"],"mappings":"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","names":["resolve","chalk","join","writeFile","mkdir","join","dirname","existsSync","join","writeFile","mkdir","resolve","chalk"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts","../../src/cli/commands/init.ts","../../src/cli/templates/doxla-workflow.ts","../../src/cli/commands/build.ts","../../src/cli/lib/discover.ts","../../src/cli/lib/manifest.ts","../../src/cli/lib/build-app.ts","../../src/cli/lib/template.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { buildCommand } from \"./commands/build.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"doxla\")\n .description(\"Improve documentation discoverability within repos\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"init\")\n .description(\"Set up GitHub Actions workflow for Doxla docs deployment\")\n .action(initCommand);\n\nprogram\n .command(\"build\")\n .description(\"Discover markdown files and build the docs viewer\")\n .option(\"-o, --output <dir>\", \"Output directory\", \"doxla-dist\")\n .option(\"-r, --root <dir>\", \"Root directory to scan for markdown files\", \".\")\n .option(\"--base-path <path>\", \"Base path for GitHub Pages deployment\", \"/\")\n .action(buildCommand);\n\nprogram.parse();\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport chalk from \"chalk\";\nimport { workflowTemplate } from \"../templates/doxla-workflow.js\";\n\nexport async function initCommand() {\n const workflowDir = join(process.cwd(), \".github\", \"workflows\");\n const workflowPath = join(workflowDir, \"doxla.yml\");\n\n if (existsSync(workflowPath)) {\n console.log(\n chalk.yellow(\"⚠ Workflow file already exists at .github/workflows/doxla.yml\")\n );\n console.log(chalk.yellow(\" Overwriting with latest template...\"));\n }\n\n await mkdir(workflowDir, { recursive: true });\n await writeFile(workflowPath, workflowTemplate, \"utf-8\");\n\n console.log(chalk.green(\"✓ Created .github/workflows/doxla.yml\"));\n console.log();\n console.log(\"Next steps:\");\n console.log(\n ` 1. Enable GitHub Pages in your repo settings (Settings → Pages → Source: ${chalk.bold(\"GitHub Actions\")})`\n );\n console.log(\" 2. Commit and push the workflow file\");\n console.log(\n \" 3. Your docs will be built and deployed on every push to main\"\n );\n}\n","export const workflowTemplate = `name: Deploy Doxla Docs\n\non:\n push:\n branches: [main]\n workflow_dispatch:\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\nconcurrency:\n group: \"pages\"\n cancel-in-progress: false\n\njobs:\n build-and-deploy:\n runs-on: ubuntu-latest\n environment:\n name: github-pages\n url: \\${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: \"20\"\n - run: npx doxla@latest build --base-path \"/\\${{ github.event.repository.name }}\"\n - uses: actions/upload-pages-artifact@v3\n with:\n path: ./doxla-dist\n - id: deployment\n uses: actions/deploy-pages@v4\n`;\n","import { resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { discoverMarkdownFiles } from \"../lib/discover.js\";\nimport { generateManifest } from \"../lib/manifest.js\";\nimport { buildApp } from \"../lib/build-app.js\";\n\ninterface BuildOptions {\n output: string;\n root: string;\n basePath: string;\n}\n\nexport async function buildCommand(options: BuildOptions) {\n const rootDir = resolve(options.root);\n const outputDir = resolve(options.output);\n const basePath = options.basePath;\n\n console.log(chalk.bold(\"doxla build\"));\n console.log();\n\n // Step 1: Discover markdown files\n const discoverSpinner = ora(\"Discovering markdown files...\").start();\n const files = await discoverMarkdownFiles(rootDir);\n\n if (files.length === 0) {\n discoverSpinner.fail(\"No markdown files found\");\n process.exit(1);\n }\n\n discoverSpinner.succeed(`Found ${files.length} markdown file${files.length === 1 ? \"\" : \"s\"}`);\n\n // Step 2: Generate manifest\n const manifestSpinner = ora(\"Generating manifest...\").start();\n const manifest = await generateManifest(rootDir, files);\n manifestSpinner.succeed(\"Manifest generated\");\n\n // Step 3: Build docs viewer\n const buildSpinner = ora(\"Building docs viewer...\").start();\n try {\n await buildApp(manifest, { output: outputDir, basePath });\n buildSpinner.succeed(\"Docs viewer built\");\n } catch (error) {\n buildSpinner.fail(\"Build failed\");\n console.error(chalk.red((error as Error).message));\n process.exit(1);\n }\n\n console.log();\n console.log(chalk.green(`✓ Output written to ${options.output}/`));\n}\n","import fg from \"fast-glob\";\n\nconst IGNORE_PATTERNS = [\n \"**/node_modules/**\",\n \"**/.git/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/doxla-dist/**\",\n \"**/.next/**\",\n \"**/coverage/**\",\n];\n\nexport async function discoverMarkdownFiles(rootDir: string): Promise<string[]> {\n const files = await fg(\"**/*.{md,mdx}\", {\n cwd: rootDir,\n ignore: IGNORE_PATTERNS,\n onlyFiles: true,\n });\n\n // Sort: README.md/README.mdx first, then alphabetical\n return files.sort((a, b) => {\n const aIsReadme = /^readme\\.mdx?$/i.test(a);\n const bIsReadme = /^readme\\.mdx?$/i.test(b);\n if (aIsReadme && !bIsReadme) return -1;\n if (!aIsReadme && bIsReadme) return 1;\n return a.localeCompare(b);\n });\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join, basename, dirname } from \"node:path\";\nimport type { Manifest, DocFile } from \"../../app/src/types/manifest.js\";\n\nfunction extractTitle(content: string, filePath: string): string {\n // Try to extract from first # heading\n const match = content.match(/^#\\s+(.+)$/m);\n if (match) {\n return match[1].trim();\n }\n\n // Fallback to filename without extension\n const ext = filePath.endsWith(\".mdx\") ? \".mdx\" : \".md\";\n const name = basename(filePath, ext);\n if (name.toLowerCase() === \"readme\") {\n const dir = dirname(filePath);\n if (dir === \".\") return \"README\";\n return `${dir} - README`;\n }\n return name.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction createSlug(filePath: string): string {\n return filePath\n .replace(/\\.mdx?$/i, \"\")\n .replace(/\\\\/g, \"/\")\n .toLowerCase()\n .replace(/[^a-z0-9/.-]/g, \"-\");\n}\n\nexport async function generateManifest(\n rootDir: string,\n files: string[]\n): Promise<Manifest> {\n const docs: DocFile[] = await Promise.all(\n files.map(async (filePath) => {\n const fullPath = join(rootDir, filePath);\n const content = await readFile(fullPath, \"utf-8\");\n return {\n slug: createSlug(filePath),\n path: filePath,\n title: extractTitle(content, filePath),\n content,\n };\n })\n );\n\n const repoName = basename(rootDir);\n\n return {\n repoName,\n generatedAt: new Date().toISOString(),\n totalDocs: docs.length,\n docs,\n };\n}\n","import { cp, writeFile, rm, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport { execSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport type { Manifest } from \"../../app/src/types/manifest.js\";\nimport { getTemplatePath } from \"./template.js\";\n\ninterface BuildOptions {\n output: string;\n basePath: string;\n}\n\nexport async function buildApp(\n manifest: Manifest,\n options: BuildOptions\n): Promise<void> {\n const templateDir = getTemplatePath();\n const tempDir = join(\n tmpdir(),\n `doxla-build-${randomBytes(6).toString(\"hex\")}`\n );\n\n try {\n // Copy template to temp dir\n await cp(templateDir, tempDir, { recursive: true });\n\n // Write manifest.json into the app's src directory\n await writeFile(\n join(tempDir, \"src\", \"manifest.json\"),\n JSON.stringify(manifest, null, 2),\n \"utf-8\"\n );\n\n // Install dependencies\n execSync(\"npm install --no-audit --no-fund\", {\n cwd: tempDir,\n stdio: \"pipe\",\n });\n\n // Build with Vite, passing base path as env var\n execSync(\"npx vite build\", {\n cwd: tempDir,\n stdio: \"pipe\",\n env: { ...process.env, VITE_BASE_PATH: options.basePath },\n });\n\n // Copy output\n await rm(options.output, { recursive: true, force: true });\n await mkdir(options.output, { recursive: true });\n await cp(join(tempDir, \"dist\"), options.output, { recursive: true });\n } finally {\n // Clean up temp dir\n await rm(tempDir, { recursive: true, force: true }).catch(() => {});\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport { dirname, resolve } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\nexport function getTemplatePath(): string {\n const __filename = fileURLToPath(import.meta.url);\n let dir = dirname(__filename);\n\n // Walk up from the current file location until we find src/app\n // Works both from source (src/cli/lib/) and built (dist/cli/)\n for (let i = 0; i < 5; i++) {\n const candidate = resolve(dir, \"src\", \"app\");\n if (existsSync(resolve(candidate, \"package.json\"))) {\n return candidate;\n }\n dir = dirname(dir);\n }\n\n // Fallback: assume built location dist/cli/ -> ../../src/app\n const fallback = resolve(dirname(__filename), \"..\", \"..\", \"src\", \"app\");\n return fallback;\n}\n"],"mappings":"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","names":["resolve","chalk","join","writeFile","mkdir","join","dirname","existsSync","join","writeFile","mkdir","resolve","chalk"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doxla",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Improve documentation discoverability within repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"ora": "^8.2.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@mdx-js/mdx": "^3.1.0",
|
|
44
45
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
46
|
"@testing-library/react": "^16.1.0",
|
|
46
47
|
"@types/node": "^22.12.0",
|
package/src/app/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DocFile } from "../types/manifest";
|
|
2
2
|
import type { Theme } from "../App";
|
|
3
3
|
import { MarkdownRenderer } from "./MarkdownRenderer";
|
|
4
|
+
import { MdxRenderer } from "./MdxRenderer";
|
|
4
5
|
import { Badge } from "./ui/Badge";
|
|
5
6
|
import { Separator } from "./ui/Separator";
|
|
6
7
|
|
|
@@ -9,8 +10,13 @@ interface DocPageProps {
|
|
|
9
10
|
theme: Theme;
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
function isMdx(path: string): boolean {
|
|
14
|
+
return path.endsWith(".mdx");
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
export function DocPage({ doc, theme }: DocPageProps) {
|
|
13
18
|
const pathParts = doc.path.split("/");
|
|
19
|
+
const mdx = isMdx(doc.path);
|
|
14
20
|
|
|
15
21
|
return (
|
|
16
22
|
<div>
|
|
@@ -32,12 +38,16 @@ export function DocPage({ doc, theme }: DocPageProps) {
|
|
|
32
38
|
|
|
33
39
|
<div className="mb-4 flex items-center gap-3">
|
|
34
40
|
<h1 className="text-3xl font-bold">{doc.title}</h1>
|
|
35
|
-
<Badge variant="secondary"
|
|
41
|
+
<Badge variant="secondary">{mdx ? ".mdx" : ".md"}</Badge>
|
|
36
42
|
</div>
|
|
37
43
|
|
|
38
44
|
<Separator className="mb-6" />
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
{mdx ? (
|
|
47
|
+
<MdxRenderer content={doc.content} theme={theme} docPath={doc.path} />
|
|
48
|
+
) : (
|
|
49
|
+
<MarkdownRenderer content={doc.content} theme={theme} docPath={doc.path} />
|
|
50
|
+
)}
|
|
41
51
|
</div>
|
|
42
52
|
);
|
|
43
53
|
}
|
|
@@ -5,6 +5,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
|
5
5
|
import { oneLight, oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
6
6
|
import type { Components } from "react-markdown";
|
|
7
7
|
import type { Theme } from "../App";
|
|
8
|
+
import { resolveDocLink } from "../lib/doc-links";
|
|
8
9
|
|
|
9
10
|
interface MarkdownRendererProps {
|
|
10
11
|
content: string;
|
|
@@ -12,29 +13,6 @@ interface MarkdownRendererProps {
|
|
|
12
13
|
docPath?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function resolveDocLink(href: string, docPath: string): string | null {
|
|
16
|
-
if (!href.match(/\.md(#.*)?$/i)) return null;
|
|
17
|
-
if (/^https?:\/\//.test(href)) return null;
|
|
18
|
-
|
|
19
|
-
const [filePart, anchor] = href.split("#");
|
|
20
|
-
const docDir = docPath.includes("/") ? docPath.replace(/\/[^/]+$/, "") : "";
|
|
21
|
-
const parts = (docDir ? `${docDir}/${filePart}` : filePart).split("/");
|
|
22
|
-
|
|
23
|
-
const resolved: string[] = [];
|
|
24
|
-
for (const part of parts) {
|
|
25
|
-
if (part === "..") resolved.pop();
|
|
26
|
-
else if (part !== ".") resolved.push(part);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const slug = resolved
|
|
30
|
-
.join("/")
|
|
31
|
-
.replace(/\.md$/i, "")
|
|
32
|
-
.toLowerCase()
|
|
33
|
-
.replace(/[^a-z0-9/.-]/g, "-");
|
|
34
|
-
|
|
35
|
-
return `#/doc/${slug}${anchor ? `#${anchor}` : ""}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
16
|
export function MarkdownRenderer({ content, theme, docPath }: MarkdownRendererProps) {
|
|
39
17
|
const syntaxStyle = theme === "dark" ? oneDark : oneLight;
|
|
40
18
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import type { ComponentType, ReactNode } from "react";
|
|
3
|
+
import * as jsxRuntime from "react/jsx-runtime";
|
|
4
|
+
import { evaluate } from "@mdx-js/mdx";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
7
|
+
import { oneLight, oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
8
|
+
import type { Theme } from "../App";
|
|
9
|
+
import { resolveDocLink } from "../lib/doc-links";
|
|
10
|
+
import { Callout } from "./mdx/Callout";
|
|
11
|
+
import { MdxErrorBoundary } from "./mdx/MdxErrorBoundary";
|
|
12
|
+
|
|
13
|
+
interface MdxRendererProps {
|
|
14
|
+
content: string;
|
|
15
|
+
theme: Theme;
|
|
16
|
+
docPath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const builtinComponents = {
|
|
20
|
+
Callout,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// WARNING: evaluate() executes arbitrary JavaScript from MDX content.
|
|
24
|
+
// Only use with trusted content from your own repository.
|
|
25
|
+
export function MdxRenderer({ content, theme, docPath }: MdxRendererProps) {
|
|
26
|
+
const [MdxContent, setMdxContent] = useState<ComponentType<{ components?: Record<string, unknown> }> | null>(null);
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
const syntaxStyle = theme === "dark" ? oneDark : oneLight;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
|
|
34
|
+
async function compileMdx() {
|
|
35
|
+
try {
|
|
36
|
+
const { default: Content } = await evaluate(content, {
|
|
37
|
+
...jsxRuntime,
|
|
38
|
+
remarkPlugins: [remarkGfm],
|
|
39
|
+
} as Parameters<typeof evaluate>[1]);
|
|
40
|
+
|
|
41
|
+
if (!cancelled) {
|
|
42
|
+
setMdxContent(() => Content);
|
|
43
|
+
setError(null);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (!cancelled) {
|
|
47
|
+
setError((err as Error).message);
|
|
48
|
+
setMdxContent(null);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
compileMdx();
|
|
54
|
+
return () => { cancelled = true; };
|
|
55
|
+
}, [content]);
|
|
56
|
+
|
|
57
|
+
const mdxComponents = useMemo(() => ({
|
|
58
|
+
...builtinComponents,
|
|
59
|
+
code(props: Record<string, unknown>) {
|
|
60
|
+
const { children, className, ...rest } = props;
|
|
61
|
+
const match = /language-(\w+)/.exec((className as string) || "");
|
|
62
|
+
const isInline = !match;
|
|
63
|
+
|
|
64
|
+
if (isInline) {
|
|
65
|
+
return (
|
|
66
|
+
<code
|
|
67
|
+
className="rounded bg-muted px-1.5 py-0.5 text-sm font-mono"
|
|
68
|
+
{...rest}
|
|
69
|
+
>
|
|
70
|
+
{children as ReactNode}
|
|
71
|
+
</code>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<SyntaxHighlighter
|
|
77
|
+
style={syntaxStyle}
|
|
78
|
+
language={match[1]}
|
|
79
|
+
PreTag="div"
|
|
80
|
+
className="rounded-md text-sm"
|
|
81
|
+
>
|
|
82
|
+
{String(children).replace(/\n$/, "")}
|
|
83
|
+
</SyntaxHighlighter>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
a(props: Record<string, unknown>) {
|
|
87
|
+
const { href, children, ...rest } = props;
|
|
88
|
+
if (href && docPath) {
|
|
89
|
+
const resolved = resolveDocLink(href as string, docPath);
|
|
90
|
+
if (resolved) {
|
|
91
|
+
return <a href={resolved} {...rest}>{children as ReactNode}</a>;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return <a href={href as string} {...rest}>{children as ReactNode}</a>;
|
|
95
|
+
},
|
|
96
|
+
}), [syntaxStyle, docPath]);
|
|
97
|
+
|
|
98
|
+
if (error) {
|
|
99
|
+
return (
|
|
100
|
+
<div className="rounded-md border border-red-300 bg-red-50 p-4 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-300">
|
|
101
|
+
<p className="font-semibold">MDX Error</p>
|
|
102
|
+
<pre className="mt-2 text-sm whitespace-pre-wrap">{error}</pre>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!MdxContent) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="animate-pulse space-y-3">
|
|
110
|
+
<div className="h-6 w-1/3 rounded bg-muted" />
|
|
111
|
+
<div className="h-4 w-full rounded bg-muted" />
|
|
112
|
+
<div className="h-4 w-2/3 rounded bg-muted" />
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className={`prose prose-neutral max-w-none ${theme === "dark" ? "prose-invert" : ""}`}>
|
|
119
|
+
<MdxErrorBoundary>
|
|
120
|
+
<MdxContent components={mdxComponents} />
|
|
121
|
+
</MdxErrorBoundary>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface CalloutProps {
|
|
5
|
+
type?: "info" | "warning" | "danger";
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const styles = {
|
|
10
|
+
info: "border-blue-500/40 bg-blue-50 text-blue-900 dark:bg-blue-950/30 dark:text-blue-200",
|
|
11
|
+
warning: "border-yellow-500/40 bg-yellow-50 text-yellow-900 dark:bg-yellow-950/30 dark:text-yellow-200",
|
|
12
|
+
danger: "border-red-500/40 bg-red-50 text-red-900 dark:bg-red-950/30 dark:text-red-200",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const labels: Record<string, string> = {
|
|
16
|
+
info: "Info",
|
|
17
|
+
warning: "Warning",
|
|
18
|
+
danger: "Danger",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function Callout({ type = "info", children }: CalloutProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className={cn("my-4 rounded-md border-l-4 p-4", styles[type])}>
|
|
24
|
+
<p className="mb-1 text-sm font-semibold">{labels[type]}</p>
|
|
25
|
+
<div className="text-sm [&>p]:m-0">{children}</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component } from "react";
|
|
2
|
+
import type { ReactNode, ErrorInfo } from "react";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface State {
|
|
9
|
+
error: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MdxErrorBoundary extends Component<Props, State> {
|
|
13
|
+
constructor(props: Props) {
|
|
14
|
+
super(props);
|
|
15
|
+
this.state = { error: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDerivedStateFromError(error: Error): State {
|
|
19
|
+
return { error: error.message };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
23
|
+
console.error("MDX runtime error:", error, info);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
componentDidUpdate(prevProps: Props) {
|
|
27
|
+
if (prevProps.children !== this.props.children && this.state.error) {
|
|
28
|
+
this.setState({ error: null });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
render() {
|
|
33
|
+
if (this.state.error) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="rounded-md border border-red-300 bg-red-50 p-4 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-300">
|
|
36
|
+
<p className="font-semibold">MDX Runtime Error</p>
|
|
37
|
+
<pre className="mt-2 text-sm whitespace-pre-wrap">{this.state.error}</pre>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return this.props.children;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function resolveDocLink(href: string, docPath: string): string | null {
|
|
2
|
+
if (!href.match(/\.mdx?(#.*)?$/i)) return null;
|
|
3
|
+
if (/^https?:\/\//.test(href)) return null;
|
|
4
|
+
|
|
5
|
+
const [filePart, anchor] = href.split("#");
|
|
6
|
+
const docDir = docPath.includes("/") ? docPath.replace(/\/[^/]+$/, "") : "";
|
|
7
|
+
const parts = (docDir ? `${docDir}/${filePart}` : filePart).split("/");
|
|
8
|
+
|
|
9
|
+
const resolved: string[] = [];
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (part === "..") {
|
|
12
|
+
if (resolved.length > 0) resolved.pop();
|
|
13
|
+
else return null;
|
|
14
|
+
} else if (part !== ".") {
|
|
15
|
+
resolved.push(part);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const slug = resolved
|
|
20
|
+
.join("/")
|
|
21
|
+
.replace(/\.mdx?$/i, "")
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9/.-]/g, "-");
|
|
24
|
+
|
|
25
|
+
if (!slug) return null;
|
|
26
|
+
|
|
27
|
+
return `#/doc/${slug}${anchor ? `#${anchor}` : ""}`;
|
|
28
|
+
}
|