Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Create App Generator

Last updated Oct 12, 2026
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The create-fumadocs-app package provides an automated CLI scaffolding tool to set up new documentation projects configured with Fumadocs, supporting frameworks including Next.js, Waku, React Router, and TanStack Start. It addresses the complexity of configuring build-time MDX compilation, router integration, layout providers, and peripheral developer tooling by orchestrating interactive prompt groups, template asset copying, workspace dependency version alignment, and AST-based code transformations. Design decisions center around modular template plugins and AST manipulation via ts-morph to programmatically inject search integrations, AI chat components, and linter configurations directly into generated project structures without manual boilerplate intervention.

Sources: packages/create-app/src/bin.ts:1-60, packages/create-app/src/index.ts:1-126, packages/create-app/src/constants.ts:34-80, packages/create-app/src/transform/index.ts:1-174

Interactive Prompts and Scaffolding Execution

CLI Configuration and Argument Parsing

The CLI entry point uses @commander-js/extra-typings to define positional arguments and option flags before initiating interactive prompts. The root command accepts an optional [name] argument for the target project and configures flags for build options, templates, package managers, and tool integrations.

const command = program
  .argument('[name]', 'the project name')
  .option('--src', '(Next.js only) enable `src/` directory')
  .option('--install', 'install packages automatically')
  .option('--no-git', 'disable auto Git repository initialization')
  .addOption(
    new Option(
      '--linter <name>',
      'configure a linter/formatter, ESLint is currently Next.js only.',
    ).choices(['eslint', 'oxlint', 'biome']),
  )
  .addOption(
    new Option('--search <name>', 'configure a search solution').choices(['orama', 'orama-cloud']),
  )
  .addOption(
    new Option('--og-image <name>', 'configure OG image generation').choices(['next-og', 'takumi']),
  )
  .addOption(
    new Option('--ai-chat <name>', 'configure AI chat').choices([
      'openrouter',
      'llmgateway',
      'inkeep',
    ]),
  )
  .addOption(
    new Option('--template <name>', 'choose a template').choices(
      templates.map((item) => item.value),
    ),
  )
  .addOption(
    new Option('--pm <name>', 'choose a package manager')
      .choices(managers)
      .default(getPackageManager()),
  );

Sources: packages/create-app/src/bin.ts:21-54

The command line options govern project parameters such as framework features, linters, search providers, and package managers.

Option FlagArgument TypeChoices / DefaultPurpose
--srcbooleanundefinedEnable src/ directory (Next.js only)
--installbooleanundefinedInstall packages automatically
--no-gitbooleanundefinedDisable auto Git repository initialization
--linter <name>stringeslint, oxlint, biomeConfigure a linter or formatter
--search <name>stringorama, orama-cloudConfigure a search solution
--og-image <name>stringnext-og, takumiConfigure Open Graph image generation
--ai-chat <name>stringopenrouter, llmgateway, inkeepConfigure AI chat integration
--template <name>stringDynamic template listChoose a starter template
--pm <name>stringDetected package managerChoose a package manager

Sources: packages/create-app/src/bin.ts:23-54

Note

When executing in a CI environment detected via isCI, interactive prompts are bypassed entirely, falling back to default values such as untitled for project name and +next+fuma-docs-mdx for the template.

Sources: packages/create-app/src/bin.ts:65-76

Interactive Prompt Groups

Using @clack/prompts, the CLI prompts the user through a structured group flow if flags are omitted from the command line execution. Each prompt checks for existing configuration values or CI flags before rendering interactive selectors or text inputs.

const options = await group(
  {
    name: async () => {
      if (defaultName) return defaultName;
      if (isCI) return 'untitled';
 
      return text({
        message: 'Project name',
        placeholder: 'my-app',
        defaultValue: 'my-app',
      });
    },
    template: async () => {
      if (config.template) return config.template;
      if (isCI) return '+next+fuma-docs-mdx';
 
      return select<Template>({
        message: 'Choose a template',
        initialValue: '+next+fuma-docs-mdx',
        options: templates,
      });
    },
    src: async ({ results }: { results: { template?: Template } }) => {
      if (config.src !== undefined) return config.src;
      if (isCI || !results.template?.startsWith('+next')) return false;
 
      return confirm({
        message: 'Use `/src` directory?',
        initialValue: false,
      });
    },

Sources: packages/create-app/src/bin.ts:62-92

The prompt sequence evaluates template selections conditionally. For instance, the src directory prompt is restricted to templates starting with +next, and the linter options differ between Next.js templates and alternative frameworks.

Warning

If a user cancels out of the Clack prompt group using the abort shortcut, the onCancel handler triggers cancel('Installation Stopped.') and immediately terminates the process with code 0.

Sources: packages/create-app/src/bin.ts:219-223

Project Directory Initialization and Template Execution

Once prompt inputs are gathered, the core create function from packages/create-app/src/index.ts initializes the destination directory, applies registered template plugins, mirrors template files, updates package.json configurations, and sets up dependencies or Git repositories.

export async function create(createOptions: Options): Promise<void> {
  const {
    outputDir,
    plugins = [],
    packageManager = 'npm',
    initializeGit = false,
    installDeps = false,
    log = console.log,
  } = createOptions;
 
  let template = templates.find((item) => item.value === createOptions.template)!;
  for (const plugin of plugins) {
    template = (await plugin.template?.call({ dest: outputDir }, template)) ?? template;
  }
 
  const appDir = path.join(outputDir, template.appDir);
  const projectName = path.basename(outputDir);
  const pluginContext: TemplatePluginContext = {
    template,
    dest: outputDir,
    log,
    appDir,
  };
 
  await copy(path.join(sourceDir, 'template', template.value), outputDir, {
    rename(file) {
      file = file.replace('example.gitignore', '.gitignore');
 
      return template.rename?.(file) ?? file;
    },
  });
 
  const packageJsonPath = path.join(outputDir, 'package.json');
  let packageJson = await initPackageJson(projectName, packageJsonPath);
  for (const plugin of plugins) {
    packageJson = (await plugin.packageJson?.call(pluginContext, packageJson)) ?? packageJson;
  }
  await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
 
  const readmePath = path.join(outputDir, 'README.md');
  let readme = `# ${projectName}\n\n${await fs.readFile(readmePath)}`;
  for (const plugin of plugins) {
    readme = (await plugin.readme?.call(pluginContext, readme)) ?? readme;
  }
  await fs.writeFile(readmePath, readme);
 
  for (const plugin of plugins) {
    await plugin.afterWrite?.call(pluginContext);
  }
 
  if (installDeps) {
    try {
      await autoInstall(packageManager, outputDir);
      log('Installed dependencies');
    } catch (err) {
      log(`Failed to install dependencies: ${err}`);
    }
  }
 
  if (initializeGit && (await tryGitInit(outputDir))) {
    log('Initialized Git repository');
  }
}

Sources: packages/create-app/src/index.ts:64-126

Tip

During template asset copying, template files named example.gitignore are automatically renamed to .gitignore, and any custom template rename hooks are executed to map source paths correctly.

Sources: packages/create-app/src/index.ts:88-94

Template Sync and Dependency Alignment

Template Asset Mirroring and Filtering

The template synchronization script (packages/create-app/scripts/sync.ts) mirrors example applications from the repository workspace into package-internal template directories (packages/create-app/template). When executing main, the script iterates across all defined template mappings, clearing existing non-README files via tinyglobby before copying fresh files from the source example directory.

const templates = [
  'react-router',
  'tanstack-start',
  'tanstack-start-spa',
  'react-router-spa',
  'waku',
  ['next', '+next+fuma-docs-mdx'],
  ['next-static', '+next+fuma-docs-mdx+static'],
];
 
async function rm(files: string[]) {
  await Promise.all(files.map((file) => fs.rm(file)));
}
 
async function main() {
  await Promise.all(
    templates.map(async (template) => {
      template = Array.isArray(template) ? template : [template, template];
      const inDir = path.join(examplesDir, template[0]);
      const outDir = path.join(templateDir, template[1]);
 
      await rm(
        await glob(['**/*', '!README.md'], {
          cwd: outDir,
          absolute: true,
        }),
      );
 

Sources: packages/create-app/scripts/sync.ts:11-38

The synchronization process relies on filter functions and file-renaming rules during asset copying. Directories such as node_modules, dist, out, and build are excluded, along with hidden files starting with a dot. Specific build artifacts and environment files like next-env.d.ts, pages.gen.ts, routeTree.gen.ts, README.md, and tsconfig.tsbuildinfo are filtered out, whereas .gitignore files are explicitly renamed to example.gitignore so they safely check in without affecting package publishing.

Source Filename / PatternFilter / Rename ActionTarget Treatment in Template
node_modules, dist, out, buildfilterDir returns falseExcluded entirely from copy
Hidden directories/files (.*)filterDir / filter returns falseExcluded unless explicitly allowed
.gitignorerename maps to example.gitignore, filter returns trueCopied and renamed to prevent local git ignore collisions
next-env.d.ts, pages.gen.ts, routeTree.gen.ts, README.md, tsconfig.tsbuildinfofilter returns falseExcluded from generated templates

Sources: packages/create-app/scripts/sync.ts:39-77

Note

Template definitions explicitly map source directories in ../../examples to their respective CLI template identifiers, supporting Next.js, React Router, Waku, and TanStack Start configurations.

Sources: packages/create-app/scripts/sync.ts:11-19

Workspace Package Version Replacement

When scaffolding a new project, the initPackageJson function in packages/create-app/src/index.ts reads the target package.json file, updates the project name, appends the postinstall script fumadocs-mdx, and replaces any workspace-protocol dependencies with explicit version numbers mapped from package constants.

async function initPackageJson(
  projectName: string,
  packageJsonPath: string,
): Promise<PackageJsonType> {
  function replaceWorkspaceDeps(deps: Record<string, string> = {}) {
    for (const k in deps) {
      if (deps[k].startsWith('workspace:') && k in depVersions) {
        deps[k] = depVersions[k as keyof typeof depVersions];
      }
    }
 
    return deps;
  }
 
  const packageJson: PackageJsonType = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
 
  return {
    ...packageJson,
    name: projectName,
    scripts: {
      ...packageJson.scripts,
      postinstall: 'fumadocs-mdx',
    },
    dependencies: replaceWorkspaceDeps(packageJson.dependencies),
    devDependencies: replaceWorkspaceDeps(packageJson.devDependencies),
  };
}

Sources: packages/create-app/src/index.ts:128-154

The workspace dependency replacement helper (replaceWorkspaceDeps) scans dependency and devDependency records. Any entry whose version string starts with workspace: and whose package name exists in depVersions has its value overwritten by the resolved version constant.

Warning

If a dependency specifies a workspace: protocol prefix but its package name is missing from depVersions, the version string remains unmodified, which can cause package manager resolution failures during installation.

Sources: packages/create-app/src/index.ts:132-140

Dependency Constants and Version Tracking

Dependency versions are aggregated inside packages/create-app/src/constants.ts by combining external dependencies declared in packages/create-app-versions/package.json with workspace package versions extracted directly from core workspace package manifests.

import { fileURLToPath } from 'node:url';
import versionPkg from '../../create-app-versions/package.json';
import * as corePkg from '../../core/package.json';
import * as mdxPkg from '../../mdx/package.json';
import * as radixPkg from '../../radix-ui/package.json';
import * as basePkg from '../../base-ui/package.json';
 

Sources: packages/create-app/src/constants.ts:1-7

The version aggregator iterates over workspace packages (corePkg, mdxPkg, radixPkg, basePkg) and injects their current versions into the depVersions record exported for scaffolding operations.

const workspaces = [corePkg, mdxPkg, radixPkg, basePkg];
 
export const depVersions = versionPkg.dependencies;
 
for (const workspace of workspaces) {
  depVersions[workspace.name as keyof typeof depVersions] = workspace.version;

Sources: packages/create-app/src/constants.ts:82-87

The baseline versions defined in packages/create-app-versions/package.json supply default dependency specifiers for external tooling and libraries.

"dependencies": {
  "@biomejs/biome": "^2.4.16",
  "@orama/core": "^1.2.19",
  "@takumi-rs/image-response": "^1.8.1",
  "fumadocs-core": "workspace:*",
  "fumadocs-mdx": "workspace:*",
  "fumadocs-ui": "workspace:*",
  "oxlint": "^1.69.0",
  "react": "^19.2.7"
}

Sources: packages/create-app-versions/package.json:6-15

Linter and Tooling Integrations

ESLint, Biome, and Oxlint Plugins

Template plugins for quality and formatting tools automate both script injections and configuration file generation during project scaffolding. Each linter plugin hooks into the template lifecycle by mutating package manifests through packageJson and writing out configuration files in afterWrite.

PluginPackage / Config TargetLint Script CommandAdded Dev Dependencies
eslinteslint.config.mjseslinteslint: ^9.39.4, eslint-config-next
biomebiome.jsonbiome check@biomejs/biome
oxlint.oxlintrc.jsonoxlintoxlint

Sources: packages/create-app/src/plugins/eslint.ts:30-35, packages/create-app/src/plugins/biome.ts:15-20, packages/create-app/src/plugins/oxlint.ts:53-57

The ESLint plugin restricts execution strictly to Next.js templates by checking this.template.value.startsWith('+next'), writing an eslint.config.mjs configuration file that consumes eslint-config-next/core-web-vitals alongside global ignores for build directories.

Sources: packages/create-app/src/plugins/eslint.ts:5-17, packages/create-app/src/plugins/eslint.ts:24-40

Warning

The ESLint plugin returns early without modifying package manifests or writing files if the selected template does not start with the +next prefix.

Sources: packages/create-app/src/plugins/eslint.ts:24-40

The Biome plugin generates a biome.json configuration file by conditionally picking either next or base rulesets depending on the template prefix, and registers lint (biome check) and format (biome format --write) scripts.

Sources: packages/create-app/src/plugins/biome.ts:13-17, packages/create-app/src/plugins/biome.ts:25-27

The Oxlint plugin configures .oxlintrc.json with schema validation pointing to ./node_modules/oxlint/configuration_schema.json, enabling typescript, react, and import plugins by default, with nextjs added under Next.js template variants.

Sources: packages/create-app/src/plugins/oxlint.ts:10-44, packages/create-app/src/plugins/oxlint.ts:62-64

Next.js Takumi Asset Presets

For Next.js templates, the nextUseTakumi plugin swaps out standard image-generation bindings for the Takumi Rust-powered image response engine. When active, it injects @takumi-rs/image-response into project dependencies and applies three distinct file transformations during the afterWrite phase.

async afterWrite() {
  if (this.template.value.startsWith('+next')) {
    await replaceImports(this);
    await replaceImagePath(this);
    await nextConfigExternal(this);
  }
},

Sources: packages/create-app/src/plugins/next-use-takumi.ts:21-27

Target FileModification MethodReplacement Details
app/og/docs/[...slug]/route.tsxreplaceAll / replaceSubstitutes next/og with @takumi-rs/image-response, updates UI import to fumadocs-ui/og/takumi, and adds format: 'webp' next to image dimensions.
lib/source.tsreplaceAllReplaces occurrences of image.png with image.webp.
next.config.mjsreplaceInserts serverExternalPackages: ['@takumi-rs/image-response'] into the exported configuration object.

Sources: packages/create-app/src/plugins/next-use-takumi.ts:31-61

Note

nextConfigExternal locates the const config = { anchor inside next.config.mjs and injects the server external packages array directly beneath it.

Sources: packages/create-app/src/plugins/next-use-takumi.ts:50-60

AST Route Manipulation for Frameworks

TanStack Start Route and Prerender Configuration

Fumadocs utilizes ts-morph to inspect and programmatically manipulate TypeScript and JavaScript configuration files across different frameworks. When configuring project options like Orama Cloud search integrations, plugins modify route files and Vite or React Router configuration files in place.

Sources: packages/create-app/src/transform/index.ts:1-12, packages/create-app/src/transform/tanstack-start.ts:1-3

For TanStack Start templates, the transformation pipeline updates vite.config.ts to register prerendered search paths by analyzing the AST structure of the configuration file.

  1. main invokes oramaCloud plugin execution during project generation. Sources: packages/create-app/src/plugins/orama-cloud.ts:37-49
  2. oramaCloud delegates to tanstackStartRoutes with a callback modifying route structures. Sources: packages/create-app/src/plugins/orama-cloud.ts:38-49
  3. tanstackStartRoutes calls addTanstackPrerender when prerender is set to true on a route option. Sources: packages/create-app/src/transform/index.ts:164-166
  4. addTanstackPrerender evaluates toItem to format path objects for the prerender configuration array. Sources: packages/create-app/src/transform/tanstack-start.ts:19-21
  5. getTanstackStartCall locates the tanstackStart plugin call expression inside the default export of vite.config.ts. Sources: packages/create-app/src/transform/tanstack-start.ts:54-69

Sources: packages/create-app/src/plugins/orama-cloud.ts:37-49, packages/create-app/src/transform/index.ts:151-174, packages/create-app/src/transform/tanstack-start.ts:9-49

Caution

If the pages property is missing from the options argument passed to the tanstackStart plugin call, addTanstackPrerender will dynamically add a new pages property containing the target routes.

Sources: packages/create-app/src/transform/tanstack-start.ts:46-48

React Router Route Filtering and Normalization

React Router route updates modify both routes.ts and react-router.config.ts. The reactRouterRoutes wrapper normalizes path arguments by splitting segments and filtering out empty entries before matching or removing elements.

Sources: packages/create-app/src/transform/index.ts:71-116

Modification UtilityTarget FileAction Performed
addRouteroutes.tsInserts a new route configuration into the route manifest.
removeRouteroutes.tsDeletes the physical route entry file from disk and filters out matching entries.
filterReactRouterPrerenderArrayreact-router.config.tsRemoves paths matching normalized patterns from excluded and paths prerender arrays.

Sources: packages/create-app/src/transform/index.ts:83-112

Important

When removing a route via removeRoute, path comparison relies on normalizePath which strips empty path segments to prevent mismatch errors caused by leading or trailing slashes.

Sources: packages/create-app/src/transform/index.ts:79-99

Search and Registry Add-On Configuration

Orama Cloud Integration and Sync Script Generation

The oramaCloud template plugin customizes scaffolded projects by injecting third-party search capabilities, adding @orama/core to package dependencies, and appending documentation notes to the project README. Sources: packages/create-app/src/plugins/orama-cloud.ts:8-29

During the post-writing phase, oramaCloud copies template asset files to the destination and root provider directories, and injects search dialog components via rootProvider. Sources: packages/create-app/src/plugins/orama-cloud.ts:30-35

It also handles framework-specific route generation for pre-rendered search indices by removing default API search endpoints and creating static JSON index routes across Next.js, TanStack Start, React Router, and Waku frameworks. Sources: packages/create-app/src/plugins/orama-cloud.ts:37-65

Template TargetPre-rendered Static JSON Path
+next+fuma-docs-mdx.next/server/app/static.json.body
tanstack-start.output/public/static.json
react-routerbuild/client/static.json
wakudist/public/static.json

Sources: packages/create-app/src/plugins/orama-cloud.ts:67-75

Furthermore, oramaCloud generates a content synchronization script at scripts/sync-content.ts that reads the pre-rendered index and synchronizes records with Orama Cloud. Sources: packages/create-app/src/plugins/orama-cloud.ts:77-104

Note

The generated sync script automatically appends && bun scripts/sync-content.ts to the project's build script in package.json. Sources: packages/create-app/src/plugins/orama-cloud.ts:13-16

AI Chat Component Installation and Environment Configuration

The ai plugin supports three distinct LLM providers: openrouter, llmgateway, and inkeep. It initializes component installations using the Fumadocs component installer connected to the remote registry. Sources: packages/create-app/src/plugins/ai.ts:10-25

Provider KeyEnvironment Variable Name
openrouterOPENROUTER_API_KEY
llmgatewayLLM_GATEWAY_API_KEY
inkeepINKEEP_API_KEY

Sources: packages/create-app/src/plugins/ai.ts:10-14

After installing required dependencies, the plugin writes an empty environment variable key to .env.local and invokes addAIChat to inject AI search components into documentation layout files. Sources: packages/create-app/src/plugins/ai.ts:26-31

Using ts-morph, addAIChat locates the DocsLayout JSX element within layout files across different frameworks and prepends the <AISearch> component tree containing <AISearchPanel> and <AISearchTrigger>. Sources: packages/create-app/src/plugins/ai.ts:39-87

It then inserts import declarations for AISearch, MessageCircleIcon, cn, and buttonVariants. Sources: packages/create-app/src/plugins/ai.ts:89-106

Warning

If DocsLayout cannot be matched as an opening JSX element child within the target source file, the AST traversal skips child injection entirely. Sources: packages/create-app/src/plugins/ai.ts:77-80

Was this page helpful?