Skip to content
Merged
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
85 changes: 61 additions & 24 deletions framework/astro/buildAstroConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,80 @@ async function copyInternalAssets(cacheRoot: string): Promise<void> {
);
}

function generateAstroConfigSource(config: ResolvedDocsConfig): string {
return `import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

export default defineConfig({
site: ${JSON.stringify(config.site)},
base: ${JSON.stringify(config.base)},
srcDir: "./",
vite: {
server: {
watch: {
// The generated project lives inside node_modules/trickfire-docs/ so that
// astro/@astrojs/starlight resolve correctly (see cachePaths.ts) - but Vite
// ignores node_modules/** by default for its dev-server watcher, which would
// otherwise silently break content hot-reload. Un-ignore just this path.
ignored: ["!**/node_modules/trickfire-docs/**"],
/** Recursively merges plain objects; arrays and primitives in `override` win outright. */
function deepMerge(
base: Record<string, unknown>,
override: Record<string, unknown>
): Record<string, unknown> {
const result: Record<string, unknown> = { ...base };
for (const [key, value] of Object.entries(override)) {
const existing = result[key];
result[key] =
isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value;
}
return result;
}

function generateAstroConfigSource(config: ResolvedDocsConfig): string {
const viteConfig = deepMerge(
{
// Without an explicit inline config, Vite's PostCSS support auto-discovers
// postcss.config.* by walking up the filesystem - which picks up whatever config
// the *consuming* repo's root happens to have (e.g. for its own Tailwind setup),
// even though this framework's own styles never need one.
css: { postcss: { plugins: [] } },
server: {
watch: {
// The generated project lives inside node_modules/trickfire-docs/ so that
// astro/@astrojs/starlight resolve correctly (see cachePaths.ts) - but Vite
// ignores node_modules/** by default for its dev-server watcher, which would
// otherwise silently break content hot-reload. Un-ignore just this path.
ignored: ["!**/node_modules/trickfire-docs/**"],
},
},
},
},
integrations: [
starlight({
title: ${JSON.stringify(config.name)},
description: ${JSON.stringify(config.description)},
config.advanced?.vite ?? {}
);

const starlightConfig = deepMerge(
{
title: config.name,
description: config.description,
logo: {
src: "./assets/nav-logo.png",
alt: "TrickFire Robotics Logo",
replacesTitle: true,
},
favicon: "/favicon.ico",
social: ${JSON.stringify(config.social)},
sidebar: ${JSON.stringify(config.sidebar)},
social: config.social,
sidebar: config.sidebar,
components: {
SocialIcons: "./components/SocialIcons.astro",
},
customCss: ["./styles/custom.css"],
}),
],
expressiveCode: {
// Shiki bundles the "dotenv" grammar but doesn't alias it to the "env" tag
// that ```env fences in docs almost always use, so it silently falls back
// to plain text.
shiki: { langAlias: { env: "dotenv" } },
},
},
config.advanced?.starlight ?? {}
);

return `import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";

export default defineConfig({
site: ${JSON.stringify(config.site)},
base: ${JSON.stringify(config.base)},
srcDir: "./",
vite: ${JSON.stringify(viteConfig, null, 4)},
integrations: [starlight(${JSON.stringify(starlightConfig, null, 4)})],
});
`;
}
Expand Down
6 changes: 3 additions & 3 deletions framework/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { runBuild } from "./commands/build.js";
import { runDev } from "./commands/dev.js";
import { runInit } from "./commands/init.js";

const [, , command] = process.argv;
const [, , command, ...args] = process.argv;
const projectRoot = process.cwd();

async function main(): Promise<void> {
Expand All @@ -14,10 +14,10 @@ async function main(): Promise<void> {
await runBuild(projectRoot);
break;
case "init":
await runInit(projectRoot);
await runInit(projectRoot, { force: args.includes("--force") });
break;
default:
console.error("Usage: trickfire-docs <dev|build|init>");
console.error("Usage: trickfire-docs <dev|build|init> [--force]");
process.exit(1);
}
}
Expand Down
27 changes: 19 additions & 8 deletions framework/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@ import fs from "node:fs/promises";
import path from "node:path";
import { getScaffoldDir } from "../astro/cachePaths.js";

export async function runInit(projectRoot: string): Promise<void> {
export interface InitOptions {
/** Scaffold over an existing docs/ and/or docs.config.ts instead of refusing. */
force?: boolean;
}

export async function runInit(projectRoot: string, options: InitOptions = {}): Promise<void> {
const scaffoldDir = getScaffoldDir();
const docsDest = path.join(projectRoot, "docs");
const configDest = path.join(projectRoot, "docs.config.ts");

if (existsSync(docsDest)) {
throw new Error(`${docsDest} already exists - refusing to overwrite.`);
}
if (existsSync(configDest)) {
throw new Error(`${configDest} already exists - refusing to overwrite.`);
if (!options.force) {
if (existsSync(docsDest)) {
throw new Error(
`${docsDest} already exists - refusing to overwrite. Re-run with --force to scaffold anyway.`
);
}
if (existsSync(configDest)) {
throw new Error(
`${configDest} already exists - refusing to overwrite. Re-run with --force to scaffold anyway.`
);
}
}

await fs.cp(path.join(scaffoldDir, "docs"), docsDest, { recursive: true });
await fs.cp(path.join(scaffoldDir, "docs.config.ts"), configDest);
await fs.cp(path.join(scaffoldDir, "docs"), docsDest, { recursive: true, force: true });
await fs.cp(path.join(scaffoldDir, "docs.config.ts"), configDest, { force: true });

console.log(`Created docs/ and docs.config.ts in ${projectRoot}`);
}
14 changes: 14 additions & 0 deletions framework/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ export interface LandingItem {
link: string;
}

export interface AdvancedConfig {
/**
* Extra Starlight options, deep-merged over the framework's generated `starlight()`
* config. Values must be JSON-serializable - they're written into a generated
* astro.config.mjs, so functions/imports (e.g. a custom Vite plugin instance) won't
* survive the round-trip.
*/
starlight?: Record<string, unknown>;
/** Extra Vite options, deep-merged over the framework's generated `vite` config. Same JSON-serializability caveat as `starlight` above. */
vite?: Record<string, unknown>;
}

export interface DocsConfig {
/** Site name, shown in the nav and browser tab. */
name: string;
Expand All @@ -50,6 +62,8 @@ export interface DocsConfig {
* "name"), Notion, and TrickFire Robotics links that are always present.
*/
social?: SocialLinks;
/** Escape hatch for Starlight/Vite options this config doesn't otherwise expose. */
advanced?: AdvancedConfig;
}

export function defineConfig(config: DocsConfig): DocsConfig {
Expand Down
12 changes: 10 additions & 2 deletions init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,21 @@ fi
# ---------------------------------------------------------------------------
# pnpm-workspace.yaml
# ---------------------------------------------------------------------------
cat > pnpm-workspace.yaml <<'EOF'
if [[ -f pnpm-workspace.yaml ]]; then
warn "pnpm-workspace.yaml already exists - skipping. Make sure it includes:"
warn " allowBuilds: { esbuild: true, sharp: true }"
warn " minimumReleaseAgeExclude: [trickfire-docs]"
else
cat > pnpm-workspace.yaml <<'EOF'
allowBuilds:
esbuild: true
sharp: true

minimumReleaseAgeExclude:
- trickfire-docs
EOF
info "Created pnpm-workspace.yaml"
info "Created pnpm-workspace.yaml"
fi

# ---------------------------------------------------------------------------
# .github/workflows/pages.yml
Expand Down Expand Up @@ -152,6 +158,8 @@ pnpm install
# ---------------------------------------------------------------------------
if [[ -d docs ]] || [[ -f docs.config.ts ]]; then
warn "docs/ or docs.config.ts already exist - skipping scaffold"
warn "To scaffold the starter docs/ and docs.config.ts anyway (existing files with the same names are overwritten, others are left alone), run:"
warn " pnpm exec trickfire-docs init --force"
else
info "Scaffolding docs..."
pnpm exec trickfire-docs init
Expand Down