-
-
Notifications
You must be signed in to change notification settings - Fork 164
feat(cli): add ultracite, oxlint, fumadocs addons #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d95de9e
73708b3
db5c07e
8b0e185
f28f44d
7402fb9
4961341
a223996
b2b67ab
67ec9a3
69cbd3e
94182c6
fd08061
f6802b1
126677e
71d1f89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,15 +1,19 @@ | ||||||||||||||||||||||||||||||||||||
import path from "node:path"; | ||||||||||||||||||||||||||||||||||||
import { log } from "@clack/prompts"; | ||||||||||||||||||||||||||||||||||||
import { execa } from "execa"; | ||||||||||||||||||||||||||||||||||||
import fs from "fs-extra"; | ||||||||||||||||||||||||||||||||||||
import pc from "picocolors"; | ||||||||||||||||||||||||||||||||||||
import type { Frontend, ProjectConfig } from "../../types"; | ||||||||||||||||||||||||||||||||||||
import type { Frontend, PackageManager, ProjectConfig } from "../../types"; | ||||||||||||||||||||||||||||||||||||
import { addPackageDependency } from "../../utils/add-package-deps"; | ||||||||||||||||||||||||||||||||||||
import { getPackageExecutionCommand } from "../../utils/package-runner"; | ||||||||||||||||||||||||||||||||||||
import { setupFumadocs } from "./fumadocs-setup"; | ||||||||||||||||||||||||||||||||||||
import { setupStarlight } from "./starlight-setup"; | ||||||||||||||||||||||||||||||||||||
import { setupTauri } from "./tauri-setup"; | ||||||||||||||||||||||||||||||||||||
import { setupUltracite } from "./ultracite-setup"; | ||||||||||||||||||||||||||||||||||||
import { addPwaToViteConfig } from "./vite-pwa-setup"; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
export async function setupAddons(config: ProjectConfig, isAddCommand = false) { | ||||||||||||||||||||||||||||||||||||
const { addons, frontend, projectDir } = config; | ||||||||||||||||||||||||||||||||||||
const { addons, frontend, projectDir, packageManager } = config; | ||||||||||||||||||||||||||||||||||||
const hasReactWebFrontend = | ||||||||||||||||||||||||||||||||||||
frontend.includes("react-router") || | ||||||||||||||||||||||||||||||||||||
frontend.includes("tanstack-router") || | ||||||||||||||||||||||||||||||||||||
|
@@ -53,15 +57,38 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")} | |||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||
await setupTauri(config); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
if (addons.includes("biome")) { | ||||||||||||||||||||||||||||||||||||
await setupBiome(projectDir); | ||||||||||||||||||||||||||||||||||||
const hasUltracite = addons.includes("ultracite"); | ||||||||||||||||||||||||||||||||||||
const hasBiome = addons.includes("biome"); | ||||||||||||||||||||||||||||||||||||
const hasHusky = addons.includes("husky"); | ||||||||||||||||||||||||||||||||||||
const hasOxlint = addons.includes("oxlint"); | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
if (hasUltracite) { | ||||||||||||||||||||||||||||||||||||
await setupUltracite(config, hasHusky); | ||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||
if (hasBiome) { | ||||||||||||||||||||||||||||||||||||
await setupBiome(projectDir); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
if (hasHusky) { | ||||||||||||||||||||||||||||||||||||
// Determine which linter to use for lint-staged | ||||||||||||||||||||||||||||||||||||
let linter: "biome" | "oxlint" | undefined; | ||||||||||||||||||||||||||||||||||||
if (hasOxlint) { | ||||||||||||||||||||||||||||||||||||
linter = "oxlint"; | ||||||||||||||||||||||||||||||||||||
} else if (hasBiome) { | ||||||||||||||||||||||||||||||||||||
linter = "biome"; | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
await setupHusky(projectDir, linter); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
if (addons.includes("husky")) { | ||||||||||||||||||||||||||||||||||||
await setupHusky(projectDir); | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
if (addons.includes("oxlint")) { | ||||||||||||||||||||||||||||||||||||
await setupOxlint(projectDir, packageManager); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
if (addons.includes("starlight")) { | ||||||||||||||||||||||||||||||||||||
await setupStarlight(config); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
if (addons.includes("fumadocs")) { | ||||||||||||||||||||||||||||||||||||
await setupFumadocs(config); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
function getWebAppDir(projectDir: string, frontends: Frontend[]): string { | ||||||||||||||||||||||||||||||||||||
|
@@ -77,7 +104,7 @@ function getWebAppDir(projectDir: string, frontends: Frontend[]): string { | |||||||||||||||||||||||||||||||||||
return path.join(projectDir, "apps/web"); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
async function setupBiome(projectDir: string) { | ||||||||||||||||||||||||||||||||||||
export async function setupBiome(projectDir: string) { | ||||||||||||||||||||||||||||||||||||
await addPackageDependency({ | ||||||||||||||||||||||||||||||||||||
devDependencies: ["@biomejs/biome"], | ||||||||||||||||||||||||||||||||||||
projectDir, | ||||||||||||||||||||||||||||||||||||
|
@@ -96,7 +123,10 @@ async function setupBiome(projectDir: string) { | |||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
async function setupHusky(projectDir: string) { | ||||||||||||||||||||||||||||||||||||
export async function setupHusky( | ||||||||||||||||||||||||||||||||||||
projectDir: string, | ||||||||||||||||||||||||||||||||||||
linter?: "biome" | "oxlint", | ||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||
await addPackageDependency({ | ||||||||||||||||||||||||||||||||||||
devDependencies: ["husky", "lint-staged"], | ||||||||||||||||||||||||||||||||||||
projectDir, | ||||||||||||||||||||||||||||||||||||
|
@@ -111,11 +141,21 @@ async function setupHusky(projectDir: string) { | |||||||||||||||||||||||||||||||||||
prepare: "husky", | ||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
packageJson["lint-staged"] = { | ||||||||||||||||||||||||||||||||||||
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [ | ||||||||||||||||||||||||||||||||||||
"biome check --write .", | ||||||||||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||
if (linter === "oxlint") { | ||||||||||||||||||||||||||||||||||||
packageJson["lint-staged"] = { | ||||||||||||||||||||||||||||||||||||
"**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint", | ||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||
} else if (linter === "biome") { | ||||||||||||||||||||||||||||||||||||
packageJson["lint-staged"] = { | ||||||||||||||||||||||||||||||||||||
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [ | ||||||||||||||||||||||||||||||||||||
"biome check --write .", | ||||||||||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||
packageJson["lint-staged"] = { | ||||||||||||||||||||||||||||||||||||
"**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "", | ||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
Comment on lines
+153
to
+157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider handling the no-linter case more gracefully. Setting an empty command for lint-staged when no linter is specified may cause issues. Consider either omitting the lint-staged configuration entirely or providing a meaningful default command. - } else {
- packageJson["lint-staged"] = {
- "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "",
- };
- }
+ }
+ // Note: If no linter is specified, we don't configure lint-staged 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
@@ -157,3 +197,33 @@ async function setupPwa(projectDir: string, frontends: Frontend[]) { | |||||||||||||||||||||||||||||||||||
await addPwaToViteConfig(viteConfigTs, path.basename(projectDir)); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
async function setupOxlint(projectDir: string, packageManager: PackageManager) { | ||||||||||||||||||||||||||||||||||||
await addPackageDependency({ | ||||||||||||||||||||||||||||||||||||
devDependencies: ["oxlint"], | ||||||||||||||||||||||||||||||||||||
projectDir, | ||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
const packageJsonPath = path.join(projectDir, "package.json"); | ||||||||||||||||||||||||||||||||||||
if (await fs.pathExists(packageJsonPath)) { | ||||||||||||||||||||||||||||||||||||
const packageJson = await fs.readJson(packageJsonPath); | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
packageJson.scripts = { | ||||||||||||||||||||||||||||||||||||
...packageJson.scripts, | ||||||||||||||||||||||||||||||||||||
check: "oxlint", | ||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||
Comment on lines
+210
to
+213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Potential script name conflict with other linters. The hardcoded "check" script name may conflict with biome's check script if both addons are installed. Consider using a more specific name like "oxlint-check" or implementing logic to handle script name conflicts. - packageJson.scripts = {
- ...packageJson.scripts,
- check: "oxlint",
- };
+ packageJson.scripts = {
+ ...packageJson.scripts,
+ "oxlint-check": "oxlint",
+ }; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
const oxlintInitCommand = getPackageExecutionCommand( | ||||||||||||||||||||||||||||||||||||
packageManager, | ||||||||||||||||||||||||||||||||||||
"oxlint@latest --init", | ||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
await execa(oxlintInitCommand, { | ||||||||||||||||||||||||||||||||||||
cwd: projectDir, | ||||||||||||||||||||||||||||||||||||
env: { CI: "true" }, | ||||||||||||||||||||||||||||||||||||
shell: true, | ||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
Comment on lines
+223
to
+228
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for the oxlint initialization command. The execa command should include error handling to provide meaningful feedback if the initialization fails. - await execa(oxlintInitCommand, {
- cwd: projectDir,
- env: { CI: "true" },
- shell: true,
- });
+ try {
+ await execa(oxlintInitCommand, {
+ cwd: projectDir,
+ env: { CI: "true" },
+ shell: true,
+ });
+ } catch (error) {
+ console.warn("Failed to initialize oxlint configuration:", error);
+ // Continue execution as this is not critical for the setup
+ } 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import path from "node:path"; | ||
import { cancel, isCancel, log, select } from "@clack/prompts"; | ||
import consola from "consola"; | ||
import { execa } from "execa"; | ||
import fs from "fs-extra"; | ||
import pc from "picocolors"; | ||
import type { ProjectConfig } from "../../types"; | ||
import { getPackageExecutionCommand } from "../../utils/package-runner"; | ||
|
||
type FumadocsTemplate = | ||
| "next-mdx" | ||
| "next-content-collections" | ||
| "react-router-mdx-remote" | ||
| "tanstack-start-mdx-remote"; | ||
|
||
const TEMPLATES = { | ||
"next-mdx": { | ||
label: "Next.js: Fumadocs MDX", | ||
hint: "Recommended template with MDX support", | ||
value: "+next+fuma-docs-mdx", | ||
}, | ||
"next-content-collections": { | ||
label: "Next.js: Content Collections", | ||
hint: "Template using Next.js content collections", | ||
value: "+next+content-collections", | ||
}, | ||
"react-router-mdx-remote": { | ||
label: "React Router: MDX Remote", | ||
hint: "Template for React Router with MDX remote", | ||
value: "react-router", | ||
}, | ||
"tanstack-start-mdx-remote": { | ||
label: "Tanstack Start: MDX Remote", | ||
hint: "Template for Tanstack Start with MDX remote", | ||
value: "tanstack-start", | ||
}, | ||
} as const; | ||
|
||
export async function setupFumadocs(config: ProjectConfig) { | ||
const { packageManager, projectDir } = config; | ||
|
||
try { | ||
log.info("Setting up Fumadocs..."); | ||
|
||
const template = await select<FumadocsTemplate>({ | ||
message: "Choose a template", | ||
options: Object.entries(TEMPLATES).map(([key, template]) => ({ | ||
value: key as FumadocsTemplate, | ||
label: template.label, | ||
hint: template.hint, | ||
})), | ||
initialValue: "next-mdx", | ||
}); | ||
|
||
if (isCancel(template)) { | ||
cancel(pc.red("Operation cancelled")); | ||
process.exit(0); | ||
} | ||
|
||
const templateArg = TEMPLATES[template].value; | ||
|
||
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm bun --no-eslint`; | ||
|
||
const fumadocsInitCommand = getPackageExecutionCommand( | ||
packageManager, | ||
commandWithArgs, | ||
); | ||
|
||
await execa(fumadocsInitCommand, { | ||
cwd: path.join(projectDir, "apps"), | ||
env: { CI: "true" }, | ||
shell: true, | ||
}); | ||
|
||
const fumadocsDir = path.join(projectDir, "apps", "fumadocs"); | ||
const packageJsonPath = path.join(fumadocsDir, "package.json"); | ||
|
||
if (await fs.pathExists(packageJsonPath)) { | ||
const packageJson = await fs.readJson(packageJsonPath); | ||
packageJson.name = "fumadocs"; | ||
|
||
if (packageJson.scripts?.dev) { | ||
packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`; | ||
} | ||
|
||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); | ||
} | ||
|
||
log.success("Fumadocs setup successfully!"); | ||
} catch (error) { | ||
log.error(pc.red("Failed to set up Fumadocs")); | ||
if (error instanceof Error) { | ||
consola.error(pc.red(error.message)); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify dependency version updates are current and secure
The version updates for
@biomejs/biome
(^2.0.0 → ^2.1.2),lint-staged
(^15.5.0 → ^16.1.2), and the newoxlint
(^1.8.0) should be verified to ensure they represent the latest stable versions and don't introduce security vulnerabilities.🌐 Web query:
💡 Result:
The latest stable versions for the requested packages as of July 27, 2025, and their known security status, are as follows:
@biomejs/biome
lint-staged
oxlint
Summary Table
Caveats & Limitations
Citations:
Action Required: Validate and Correct Dependency Versions
It looks like the versions specified in apps/cli/src/constants.ts (lines 58–62) don’t align with what’s currently published on npm:
• @biomejs/biome: "^2.1.2" – npm’s latest stable is 2.0.0 (Jun 2025). Please confirm ^2.1.2 actually exists or roll back to "^2.0.0".
• lint-staged: "^16.1.2" – npm’s latest stable is 16.1.0. Update to "^16.1.0" or verify the 16.1.2 release.
• oxlint: "^1.8.0" – no release information found. Verify that v1.8.0 is published and free of advisories.
• If you intended to pin pre-releases or unpublished packages, ensure your registry configuration supports them.
• Otherwise, update these entries to the latest valid versions and rerun your vulnerability scans.
🤖 Prompt for AI Agents