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 #2021

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
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
35 changes: 35 additions & 0 deletions cli/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ interface CliFlags {
appRouter: boolean;
/** @internal Used in CI. */
dbProvider: DatabaseProvider;
/** @internal Used in CI */
eslint: boolean;
/** @internal Used in CI */
biome: boolean;
}

interface CliResults {
Expand All @@ -62,6 +66,8 @@ const defaultOptions: CliResults = {
importAlias: "~/",
appRouter: false,
dbProvider: "sqlite",
eslint: false,
biome: false,
},
databaseProvider: "sqlite",
};
Expand Down Expand Up @@ -145,6 +151,16 @@ 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"
)
/** END CI-FLAGS */
.version(getVersion(), "-v, --version", "Display the version number")
.addHelpText(
Expand Down Expand Up @@ -183,13 +199,19 @@ 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.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.biome && cliResults.flags.eslint) {
logger.warn("Incompatible combination Biome + ESLint. 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 +322,17 @@ export const runCli = async (): Promise<CliResults> => {
initialValue: "sqlite",
});
},
linter: () => {
return p.select({
message:
"Would you like to use ESLint and Prettier or Biome for linting and formatting?",
options: [
{ value: "eslint", label: "ESLint/Prettier" },
{ value: "biome", label: "Biome" },
],
initialValue: "eslint",
});
},
...(!cliResults.flags.noGit && {
git: () => {
return p.confirm({
Expand Down Expand Up @@ -341,6 +374,8 @@ 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");

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
8 changes: 7 additions & 1 deletion cli/src/installers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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";
Expand All @@ -18,6 +19,7 @@ export const availablePackages = [
"trpc",
"envVariables",
"eslint",
"biome",
"dbContainer",
] as const;
export type AvailablePackages = (typeof availablePackages)[number];
Expand Down Expand Up @@ -83,7 +85,11 @@ export const buildPkgInstallerMap = (
installer: envVariablesInstaller,
},
eslint: {
inUse: true,
inUse: packages.includes("eslint"),
installer: dynamicEslintInstaller,
},
biome: {
inUse: packages.includes("biome"),
installer: biomeInstaller,
},
});
27 changes: 11 additions & 16 deletions cli/src/installers/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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 { addPackageScript } from "~/utils/addPackageScript.js";

export const prismaInstaller: Installer = ({
projectDir,
Expand Down Expand Up @@ -65,21 +65,16 @@ export const prismaInstaller: Installer = ({
);
const clientDest = path.join(projectDir, "src/server/db.ts");

// add postinstall and push script to package.json
const packageJsonPath = path.join(projectDir, "package.json");

const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson;
packageJsonContent.scripts = {
...packageJsonContent.scripts,
postinstall: "prisma generate",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:generate": "prisma migrate dev",
"db:migrate": "prisma migrate deploy",
};
addPackageScript({
projectDir,
scripts: {
postinstall: "prisma generate",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:generate": "prisma migrate dev",
"db:migrate": "prisma migrate deploy",
},
});

fs.copySync(clientSrc, clientDest);
fs.writeJSONSync(packageJsonPath, packageJsonContent, {
spaces: 2,
});
};
Loading
Loading