Skip to content
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

feat: Support biome.js as a linter / formatter option in the cli and add a ESLint mixed config option #2025

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/late-tips-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-t3-app": minor
---

Added support for biome.js as a formatter and linter
53 changes: 53 additions & 0 deletions cli/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ interface CliFlags {
appRouter: boolean;
/** @internal Used in CI. */
dbProvider: DatabaseProvider;
/** @internal Used in CI */
eslint: boolean;
/** @internal Used in CI */
biome: boolean;
/** @internal Used in CI */
mixedBiome: boolean;
}

interface CliResults {
Expand All @@ -62,6 +68,9 @@ const defaultOptions: CliResults = {
importAlias: "~/",
appRouter: false,
dbProvider: "sqlite",
eslint: false,
biome: false,
mixedBiome: false,
},
databaseProvider: "sqlite",
};
Expand Down Expand Up @@ -145,6 +154,21 @@ export const runCli = async (): Promise<CliResults> => {
"Explicitly tell the CLI to use the new Next.js app router",
(value) => !!value && value !== "false"
)
.option(
"--eslint [boolean]",
"Experimental: Boolean value if we should install eslint and prettier. Must be used in conjunction with `--CI`.",
(value) => !!value && value !== "false"
)
.option(
"--biome [boolean]",
"Experimental: Boolean value if we should install biome. Must be used in conjunction with `--CI`.",
(value) => !!value && value !== "false"
)
.option(
"--mixedBiome [boolean]",
"Experimental: Boolean value if we should install biome and eslint. Must be used in conjunction with `--CI`.",
(value) => !!value && value !== "false"
)
/** END CI-FLAGS */
.version(getVersion(), "-v, --version", "Display the version number")
.addHelpText(
Expand Down Expand Up @@ -183,13 +207,27 @@ export const runCli = async (): Promise<CliResults> => {
if (cliResults.flags.prisma) cliResults.packages.push("prisma");
if (cliResults.flags.drizzle) cliResults.packages.push("drizzle");
if (cliResults.flags.nextAuth) cliResults.packages.push("nextAuth");
if (cliResults.flags.eslint) cliResults.packages.push("eslint");
if (cliResults.flags.biome) cliResults.packages.push("biome");
if (cliResults.flags.mixedBiome) cliResults.packages.push("mixedBiome");
if (cliResults.flags.prisma && cliResults.flags.drizzle) {
// We test a matrix of all possible combination of packages in CI. Checking for impossible
// combinations here and exiting gracefully is easier than changing the CI matrix to exclude
// invalid combinations. We are using an "OK" exit code so CI continues with the next combination.
logger.warn("Incompatible combination Prisma + Drizzle. Exiting.");
process.exit(0);
}

if (
(cliResults.flags.mixedBiome && cliResults.flags.biome) ||
cliResults.flags.eslint
) {
logger.warn(
"Incompatible combination Biome + ESLint. Please select one or the mixed one. Exiting."
);
process.exit(0);
}

if (databaseProviders.includes(cliResults.flags.dbProvider) === false) {
logger.warn(
`Incompatible database provided. Use: ${databaseProviders.join(", ")}. Exiting.`
Expand Down Expand Up @@ -300,6 +338,18 @@ export const runCli = async (): Promise<CliResults> => {
initialValue: "sqlite",
});
},
linter: () => {
return p.select({
message:
"Would you like to use ESLint and Prettier or ESLint and Biome or only Biome for linting and formatting?",
options: [
{ value: "eslint", label: "ESLint/Prettier" },
{ value: "biome", label: "Biome" },
{ value: "mixedBiome", label: "ESLint + Biome" },
],
initialValue: "eslint",
});
},
...(!cliResults.flags.noGit && {
git: () => {
return p.confirm({
Expand Down Expand Up @@ -341,6 +391,9 @@ export const runCli = async (): Promise<CliResults> => {
if (project.authentication === "next-auth") packages.push("nextAuth");
if (project.database === "prisma") packages.push("prisma");
if (project.database === "drizzle") packages.push("drizzle");
if (project.linter === "eslint") packages.push("eslint");
if (project.linter === "biome") packages.push("biome");
if (project.linter === "mixedBiome") packages.push("mixedBiome");

return {
appName: project.name ?? cliResults.appName,
Expand Down
30 changes: 30 additions & 0 deletions cli/src/installers/biome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import path from "path";
import fs from "fs-extra";

import { PKG_ROOT } from "~/consts.js";
import { type Installer } from "~/installers/index.js";
import { addPackageDependency } from "~/utils/addPackageDependency.js";
import { addPackageScript } from "~/utils/addPackageScript.js";

export const biomeInstaller: Installer = ({ projectDir }) => {
addPackageDependency({
projectDir,
dependencies: ["@biomejs/biome"],
devMode: true,
});

const extrasDir = path.join(PKG_ROOT, "template/extras");
const biomeConfigSrc = path.join(extrasDir, "config/biome.jsonc");
const biomeConfigDest = path.join(projectDir, "biome.jsonc");

fs.copySync(biomeConfigSrc, biomeConfigDest);

addPackageScript({
projectDir,
scripts: {
"format:unsafe": "biome check --write --unsafe .",
"format:write": "biome check --write .",
"format:check": "biome check .",
},
});
};
16 changes: 13 additions & 3 deletions cli/src/installers/dependencyVersionMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const dependencyVersionMap = {
// Drizzle
"drizzle-kit": "^0.24.0",
"drizzle-orm": "^0.33.0",
"eslint-plugin-drizzle": "^0.2.3",
mysql2: "^3.11.0",
"@planetscale/database": "^1.19.0",
postgres: "^3.4.4",
Expand All @@ -25,8 +24,6 @@ export const dependencyVersionMap = {
// TailwindCSS
tailwindcss: "^3.4.3",
postcss: "^8.4.39",
prettier: "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",

// tRPC
"@trpc/client": "^11.0.0-rc.446",
Expand All @@ -36,5 +33,18 @@ export const dependencyVersionMap = {
"@tanstack/react-query": "^5.50.0",
superjson: "^2.2.1",
"server-only": "^0.0.1",

// biome
"@biomejs/biome": "1.9.4",

// eslint / prettier
prettier: "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
eslint: "^8.57.0",
"eslint-config-next": "^15.0.1",
"eslint-plugin-drizzle": "^0.2.3",
"@types/eslint": "^8.56.10",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
} as const;
export type AvailableDependencies = keyof typeof dependencyVersionMap;
33 changes: 11 additions & 22 deletions cli/src/installers/drizzle.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
import path from "path";
import fs from "fs-extra";
import { type PackageJson } from "type-fest";

import { PKG_ROOT } from "~/consts.js";
import { type Installer } from "~/installers/index.js";
import { addPackageDependency } from "~/utils/addPackageDependency.js";
import { type AvailableDependencies } from "./dependencyVersionMap.js";
import { addPackageScript } from "~/utils/addPackageScript.js";

export const drizzleInstaller: Installer = ({
projectDir,
packages,
scopedAppName,
databaseProvider,
}) => {
const devPackages: AvailableDependencies[] = [
"drizzle-kit",
"eslint-plugin-drizzle",
];

addPackageDependency({
projectDir,
dependencies: devPackages,
dependencies: ["drizzle-kit"],
devMode: true,
});
addPackageDependency({
Expand Down Expand Up @@ -75,24 +69,19 @@ export const drizzleInstaller: Installer = ({
);
const clientDest = path.join(projectDir, "src/server/db/index.ts");

// add db:* scripts to package.json
const packageJsonPath = path.join(projectDir, "package.json");

const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson;
packageJsonContent.scripts = {
...packageJsonContent.scripts,
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
};
addPackageScript({
projectDir,
scripts: {
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
},
});

fs.copySync(configFile, configDest);
fs.mkdirSync(path.dirname(schemaDest), { recursive: true });
fs.writeFileSync(schemaDest, schemaContent);
fs.writeFileSync(configDest, configContent);
fs.copySync(clientSrc, clientDest);
fs.writeJSONSync(packageJsonPath, packageJsonContent, {
spaces: 2,
});
};
52 changes: 51 additions & 1 deletion cli/src/installers/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,61 @@ import path from "path";
import fs from "fs-extra";

import { _initialConfig } from "~/../template/extras/config/_eslint.js";
import { PKG_ROOT } from "~/consts.js";
import { type Installer } from "~/installers/index.js";
import { addPackageDependency } from "~/utils/addPackageDependency.js";
import { addPackageScript } from "~/utils/addPackageScript.js";
import { type AvailableDependencies } from "./dependencyVersionMap.js";

// Also installs prettier
export const dynamicEslintInstaller: Installer = ({ projectDir, packages }) => {
const usingDrizzle = !!packages?.drizzle?.inUse;
const devPackages: AvailableDependencies[] = [
"prettier",
"eslint",
"eslint-config-next",
"@types/eslint",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
];

if (packages?.tailwind.inUse) {
devPackages.push("prettier-plugin-tailwindcss");
}
if (packages?.drizzle.inUse) {
devPackages.push("eslint-plugin-drizzle");
}

addPackageDependency({
projectDir,
dependencies: devPackages,
devMode: true,
});
const extrasDir = path.join(PKG_ROOT, "template/extras");

// Prettier
let prettierSrc: string;
if (packages?.tailwind.inUse) {
prettierSrc = path.join(extrasDir, "config/_tailwind.prettier.config.js");
} else {
prettierSrc = path.join(extrasDir, "config/_prettier.config.js");
}
const prettierDest = path.join(projectDir, "prettier.config.js");

fs.copySync(prettierSrc, prettierDest);

addPackageScript({
projectDir,
scripts: {
lint: "next lint",
"lint:fix": "next lint --fix",
check: "next lint && tsc --noEmit",
"format:write": 'prettier --write "**/*.{ts,tsx,js,jsx,mdx}" --cache',
"format:check": 'prettier --check "**/*.{ts,tsx,js,jsx,mdx}" --cache',
},
});

// eslint
const usingDrizzle = !!packages?.drizzle?.inUse;
const eslintConfig = getEslintConfig({ usingDrizzle });

// Convert config from _eslint.config.json to .eslintrc.cjs
Expand Down
14 changes: 13 additions & 1 deletion cli/src/installers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { prismaInstaller } from "~/installers/prisma.js";
import { tailwindInstaller } from "~/installers/tailwind.js";
import { trpcInstaller } from "~/installers/trpc.js";
import { type PackageManager } from "~/utils/getUserPkgManager.js";
import { biomeInstaller } from "./biome.js";
import { dbContainerInstaller } from "./dbContainer.js";
import { drizzleInstaller } from "./drizzle.js";
import { dynamicEslintInstaller } from "./eslint.js";
import { mixedBiomeInstaller } from "./mixed-biome.js";

// Turning this into a const allows the list to be iterated over for programmatically creating prompt options
// Should increase extensibility in the future
Expand All @@ -18,7 +20,9 @@ export const availablePackages = [
"trpc",
"envVariables",
"eslint",
"biome",
"dbContainer",
"mixedBiome",
] as const;
export type AvailablePackages = (typeof availablePackages)[number];

Expand Down Expand Up @@ -83,7 +87,15 @@ export const buildPkgInstallerMap = (
installer: envVariablesInstaller,
},
eslint: {
inUse: true,
inUse: packages.includes("eslint"),
installer: dynamicEslintInstaller,
},
biome: {
inUse: packages.includes("biome"),
installer: biomeInstaller,
},
mixedBiome: {
inUse: packages.includes("mixedBiome"),
installer: mixedBiomeInstaller,
},
});
Loading
Loading