From 2d723333191f2f5992375e353e08aded3992a876 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 16 Jul 2026 14:58:23 -0700 Subject: [PATCH 1/3] fix: don't clobber existing pnpm-workspace.yaml in init.sh init.sh unconditionally overwrote pnpm-workspace.yaml, wiping out any allowBuilds/minimumReleaseAgeExclude a repo already had configured. Warn and skip instead, matching the existing package.json handling. --- init.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/init.sh b/init.sh index 884e56d..6184c7f 100755 --- a/init.sh +++ b/init.sh @@ -56,7 +56,12 @@ 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 @@ -64,7 +69,8 @@ allowBuilds: minimumReleaseAgeExclude: - trickfire-docs EOF -info "Created pnpm-workspace.yaml" + info "Created pnpm-workspace.yaml" +fi # --------------------------------------------------------------------------- # .github/workflows/pages.yml From 7d68ff784c8cb4f9a50e1d9760fef484d4b038e6 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 16 Jul 2026 14:58:32 -0700 Subject: [PATCH 2/3] feat: add --force flag to scaffold over an existing docs/ setup Previously, if docs/ or docs.config.ts already existed (e.g. a repo adopting this framework after already having hand-written docs), init skipped scaffolding entirely with no path forward but manual setup. `trickfire-docs init --force` now scaffolds anyway, merging the starter docs/ into what's there; init.sh points to it in its warning. --- framework/cli.ts | 6 +++--- framework/commands/init.ts | 27 +++++++++++++++++++-------- init.sh | 2 ++ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/framework/cli.ts b/framework/cli.ts index f99aba5..e12a397 100644 --- a/framework/cli.ts +++ b/framework/cli.ts @@ -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 { @@ -14,10 +14,10 @@ async function main(): Promise { await runBuild(projectRoot); break; case "init": - await runInit(projectRoot); + await runInit(projectRoot, { force: args.includes("--force") }); break; default: - console.error("Usage: trickfire-docs "); + console.error("Usage: trickfire-docs [--force]"); process.exit(1); } } diff --git a/framework/commands/init.ts b/framework/commands/init.ts index f2de494..17d3574 100644 --- a/framework/commands/init.ts +++ b/framework/commands/init.ts @@ -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 { +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 { 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}`); } diff --git a/init.sh b/init.sh index 6184c7f..1503736 100755 --- a/init.sh +++ b/init.sh @@ -158,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 From bd558754a8026a0c97d8473210e602657148ee35 Mon Sep 17 00:00:00 2001 From: Matej Stastny Date: Thu, 16 Jul 2026 14:58:46 -0700 Subject: [PATCH 3/3] fix: scope generated Vite/Starlight config and add advanced escape hatch - Set vite.css.postcss explicitly so Vite's file-based PostCSS auto-discovery doesn't climb the filesystem and pick up whatever postcss.config.* the consuming repo happens to have at its root (e.g. for its own Tailwind/shadcn setup) - this framework's own styles never need one. - Alias the ```env code fence language to Shiki's bundled "dotenv" grammar, which it doesn't recognize under that name by default and was silently falling back to plain text. - Add a JSON-serializable `advanced.starlight` / `advanced.vite` field to docs.config.ts, deep-merged into the generated config, so consumers needing one more Starlight/Vite option don't have to fork or patch this package. --- framework/astro/buildAstroConfig.ts | 85 +++++++++++++++++++++-------- framework/config/schema.ts | 14 +++++ 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/framework/astro/buildAstroConfig.ts b/framework/astro/buildAstroConfig.ts index 1b93ea5..9cdf671 100644 --- a/framework/astro/buildAstroConfig.ts +++ b/framework/astro/buildAstroConfig.ts @@ -12,43 +12,80 @@ async function copyInternalAssets(cacheRoot: string): Promise { ); } -function generateAstroConfigSource(config: ResolvedDocsConfig): string { - return `import { defineConfig } from "astro/config"; -import starlight from "@astrojs/starlight"; +function isPlainObject(value: unknown): value is Record { + 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, + override: Record +): Record { + const result: Record = { ...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)})], }); `; } diff --git a/framework/config/schema.ts b/framework/config/schema.ts index 32a42d3..bc49b8c 100644 --- a/framework/config/schema.ts +++ b/framework/config/schema.ts @@ -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; + /** Extra Vite options, deep-merged over the framework's generated `vite` config. Same JSON-serializability caveat as `starlight` above. */ + vite?: Record; +} + export interface DocsConfig { /** Site name, shown in the nav and browser tab. */ name: string; @@ -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 {