Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/docs-site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.astro
dist
src/content/docs/api
src/content/docs/generated
35 changes: 35 additions & 0 deletions packages/docs-site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# @simple-git/docs-site

Static documentation site for `simple-git` built with Astro + Starlight.

## Commands

- `yarn workspace @simple-git/docs-site dev` runs markdown sync + API generation watchers and starts the local docs server.
- `yarn workspace @simple-git/docs-site build` runs generation steps and builds static output.
- `yarn workspace @simple-git/docs-site preview` serves the built output.
- `yarn workspace @simple-git/docs-site check` runs Astro checks.

## Content Sources

- `simple-git/readme.md`
- `docs/*.md`
- `simple-git/typings/index.d.ts` with type/source resolution via `simple-git/tsconfig.release.json`

Generated files are written to:

- `src/content/docs/generated`
- `src/content/docs/api`

These paths are generated at runtime and are intentionally gitignored.

## Vercel Deployment

Configure the Vercel project in the dashboard with:

- **Framework preset:** Astro
- **Install command:** `yarn install --immutable`
- **Build command:** `yarn workspace @simple-git/docs-site build`
- **Output directory:** `packages/docs-site/dist`
- **Root directory:** repository root

No `vercel.json` is required for this phase.
8 changes: 8 additions & 0 deletions packages/docs-site/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightConfig from './starlight.config.mjs';

export default defineConfig({
output: 'static',
integrations: [starlight(starlightConfig)]
});
32 changes: 32 additions & 0 deletions packages/docs-site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@simple-git/docs-site",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"docs:sync": "node ./scripts/sync-markdown.mjs",
"docs:sync:watch": "node ./scripts/sync-markdown.mjs --watch",
"docs:api": "node ./scripts/build-api.mjs",
"docs:api:watch": "node ./scripts/build-api.mjs --watch",
"docs:api:normalize": "node ./scripts/build-api.mjs --normalize-only",
"dev": "yarn docs:sync && yarn docs:api && concurrently -k -n sync,astro \"yarn docs:sync:watch\" \"astro dev\"",
"build": "yarn docs:sync && yarn docs:api && astro build",
"preview": "astro preview",
"check": "astro check"
},
"dependencies": {
"@astrojs/starlight": "^0.31.0",
"astro": "^5.0.0",
"concurrently": "^9.2.1",
"typedoc": "^0.28.13",
"typedoc-plugin-markdown": "^4.9.0"
},
"devDependencies": {
"typescript": "^5.7.3"
},
"repository": {
"type": "git",
"url": "https://github.com/steveukx/git-js.git",
"directory": "packages/docs-site"
}
}
139 changes: 139 additions & 0 deletions packages/docs-site/scripts/build-api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { promises as fs } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { spawn } from 'node:child_process';

const watchMode = process.argv.includes('--watch');
const normalizeOnly = process.argv.includes('--normalize-only');

const workspaceRoot = path.resolve(import.meta.dirname, '..');
const repoRoot = path.resolve(workspaceRoot, '../..');
const apiOutDir = path.join(workspaceRoot, 'src/content/docs/api');
const typedocBin = path.join(repoRoot, 'node_modules/.bin/typedoc');

const typedocArgs = [
'--tsconfig',
path.join(repoRoot, 'simple-git/tsconfig.release.json'),
'--entryPoints',
path.join(repoRoot, 'simple-git/typings/index.d.ts'),
'--plugin',
'typedoc-plugin-markdown',
'--out',
apiOutDir,
'--readme',
'none',
'--excludePrivate',
'--excludeProtected',
'--excludeInternal',
'--hidePageTitle',
'--githubPages',
'false',
'--entryFileName',
'index.md',
'--flattenOutputFiles'
];

if (watchMode) {
typedocArgs.push('--watch');
}

const toTitle = (filePath) => {
const base = path.basename(filePath, '.md');
if (base === 'index') {
return 'API Reference';
}

const withoutPrefixes = base.replace(
/^(Class|Interface|Type-alias|Variable|Enumeration|Enum|Function)\./,
''
);

return withoutPrefixes.replace(/[._-]+/g, ' ');
};

const withFrontmatter = (title, body) => `---\ntitle: ${JSON.stringify(title)}\n---\n\n${body}`;

const listMarkdownFiles = async (rootDir) => {
const output = [];
const stack = [rootDir];

while (stack.length) {
const currentDir = stack.pop();
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
output.push(fullPath);
}
}
}

return output;
};

const normalizeFrontmatter = async () => {
const files = await listMarkdownFiles(apiOutDir);
let updated = 0;

for (const filePath of files) {
const body = await fs.readFile(filePath, 'utf8');
if (body.startsWith('---\n')) {
continue;
}

const title = toTitle(filePath);
await fs.writeFile(filePath, withFrontmatter(title, body), 'utf8');
updated += 1;
}

process.stdout.write(`[docs:api] normalized frontmatter in ${updated} files\n`);
};

const run = async () => {
if (normalizeOnly) {
await normalizeFrontmatter();
return;
}

try {
await fs.access(typedocBin);
} catch {
throw new Error(
`typedoc binary not found at ${typedocBin}. Run 'yarn install' at the repo root to install docs-site dependencies.`
);
}

await fs.rm(apiOutDir, { recursive: true, force: true });
await fs.mkdir(apiOutDir, { recursive: true });

await new Promise((resolve, reject) => {
const child = spawn(typedocBin, typedocArgs, {
cwd: workspaceRoot,
stdio: 'inherit',
env: process.env
});

child.on('error', reject);

child.on('exit', (code) => {
if (watchMode) {
resolve();
} else if (code === 0) {
resolve();
} else {
reject(new Error(`typedoc exited with code ${code}`));
}
});
});

if (!watchMode) {
await normalizeFrontmatter();
}
};

run().catch((error) => {
process.stderr.write(`[docs:api] ${error instanceof Error ? error.stack : String(error)}\n`);
process.exitCode = 1;
});
94 changes: 94 additions & 0 deletions packages/docs-site/scripts/sync-markdown.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { promises as fs, watch as watchFs } from 'node:fs';
import path from 'node:path';
import process from 'node:process';

const repoRoot = path.resolve(import.meta.dirname, '../../..');
const docsRoot = path.resolve(repoRoot, 'packages/docs-site/src/content/docs/generated');
const sourceReadme = path.resolve(repoRoot, 'simple-git/readme.md');
const sourceDocsDir = path.resolve(repoRoot, 'docs');

const watchMode = process.argv.includes('--watch');

const titleFromFilename = (filename) =>
filename
.replace(/\.md$/i, '')
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase());

const titleFromMarkdown = (markdown, fallback) => {
const heading = markdown.match(/^#{1,6}\s+(.+)$/m);
return heading?.[1]?.trim() || fallback;
};

const slugify = (filename) =>
filename
.replace(/\.md$/i, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');

const withFrontmatter = (title, body) => {
const safeBody = body.startsWith('---\n') ? body.replace(/^---[\s\S]*?---\n?/, '') : body;
return `---\ntitle: ${JSON.stringify(title)}\n---\n\n${safeBody}`;
};

const rebuild = async () => {
await fs.rm(docsRoot, { recursive: true, force: true });
await fs.mkdir(path.join(docsRoot, 'guides'), { recursive: true });

const readmeBody = await fs.readFile(sourceReadme, 'utf8');
const readmeTitle = titleFromMarkdown(readmeBody, 'README');
await fs.writeFile(
path.join(docsRoot, 'readme.md'),
withFrontmatter(readmeTitle, readmeBody),
'utf8'
);

const docEntries = (await fs.readdir(sourceDocsDir, { withFileTypes: true }))
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
.sort((a, b) => a.name.localeCompare(b.name));

for (const entry of docEntries) {
const sourceFile = path.join(sourceDocsDir, entry.name);
const targetFile = path.join(docsRoot, 'guides', `${slugify(entry.name)}.md`);
const sourceBody = await fs.readFile(sourceFile, 'utf8');
const title = titleFromMarkdown(sourceBody, titleFromFilename(entry.name));

await fs.writeFile(targetFile, withFrontmatter(title, sourceBody), 'utf8');
}

const generatedCount = docEntries.length + 1;
process.stdout.write(`[docs:sync] wrote ${generatedCount} markdown files\n`);
};

const main = async () => {
await rebuild();

if (!watchMode) {
return;
}

let timer;
const onChange = () => {
clearTimeout(timer);
timer = setTimeout(async () => {
try {
await rebuild();
} catch (error) {
process.stderr.write(`[docs:sync] ${error instanceof Error ? error.stack : String(error)}\n`);
}
}, 100);
};

watchFs(sourceReadme, onChange);
watchFs(sourceDocsDir, onChange);

process.stdout.write('[docs:sync] watching markdown sources\n');

process.stdin.resume();
};

main().catch((error) => {
process.stderr.write(`[docs:sync] ${error instanceof Error ? error.stack : String(error)}\n`);
process.exitCode = 1;
});
Loading
Loading