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 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b4c3f6a
docs: added appRouter CLI flag to docs
aidansunbury Oct 26, 2024
b519a8f
Merge branch 't3-oss:main' into main
aidansunbury Nov 9, 2024
e1dfc85
initial comments on edit locations
aidansunbury Nov 9, 2024
41d2811
working on being able to test initial version
aidansunbury Nov 10, 2024
bafb814
moved prettier related setup to eslint file so that it is not tied to…
aidansunbury Nov 10, 2024
d62917f
added package.json scripts for biome
aidansunbury Nov 10, 2024
2c1f7f1
added changeset
aidansunbury Nov 10, 2024
cbad9bc
fixed biome scripts and created addPackageScript function
aidansunbury Nov 15, 2024
897ae87
Merge branch 'main' into main
juliusmarminge Jan 12, 2025
e84958e
test adding lint and formating check to e2e tests
aidansunbury Jan 15, 2025
9ef765b
fix lint issues to be able to run updated e2e tests
aidansunbury Jan 15, 2025
4ed4df7
move check to correct directory to actually run tests
aidansunbury Jan 15, 2025
af52d73
make sure we can format and lint
aidansunbury Jan 15, 2025
d27c29f
run check and format in correct directory with eslint scaffolded
aidansunbury Jan 15, 2025
6ad3205
format scaffolded project after creation and check in e2e
aidansunbury Jan 15, 2025
08173ce
lint fix
aidansunbury Jan 15, 2025
9cfd0d2
revert e2e tests to ensure less than 256 matrix variations
aidansunbury Jan 15, 2025
8415dfa
remove extra parts of e2e
aidansunbury Jan 15, 2025
63534b4
add back space
aidansunbury Jan 15, 2025
59eec32
Merge branch 'main' into main
aidansunbury Jan 15, 2025
2ce9ea5
added biome.jsonc suggestions
aidansunbury Jan 16, 2025
29fdd74
Merge branch 'main' of https://github.com/aidansunbury/create-t3-app
aidansunbury Jan 16, 2025
315a9ce
Update biome.jsonc
juliusmarminge Jan 25, 2025
d9f3788
fix lint warnings in biome config
juliusmarminge Jan 25, 2025
4d68159
fixed issue with eslint / biome only being run when using flags
aidansunbury Jan 29, 2025
c0c1460
Merge branch 'main' of https://github.com/aidansunbury/create-t3-app
aidansunbury Jan 29, 2025
38e9eab
add e2e test to ensure generated project passed linting and formattin…
aidansunbury Jan 29, 2025
46727d5
added env to eslint check to pass next-lint
aidansunbury Jan 29, 2025
9bbf1ab
added eslint as a default package so that bun e2e test passes
aidansunbury Jan 29, 2025
e563aec
modify bun test to check for bun.lock instead of bun.lockb
aidansunbury Jan 30, 2025
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
54 changes: 52 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ jobs:
# has to be scaffolded outside the CLI project so that no lint/tsconfig are leaking
# through. this way it ensures that it is the app's configs that are being used
# FIXME: this is a bit hacky, would rather have --packages=trpc,tailwind,... but not sure how to setup the matrix for that
- run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} --dbProvider=${{ matrix.dbType }}
- run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} --dbProvider=${{ matrix.dbType }} --eslint
if: ${{ steps.matrix-valid.outputs.continue == 'true' }}
# can't use default mysql string cause t3-env blocks that

- run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} && pnpm build
if: ${{ steps.matrix-valid.outputs.continue == 'true' }}
env:
Expand All @@ -66,6 +67,55 @@ jobs:
AUTH_DISCORD_SECRET: baz
SKIP_ENV_VALIDATION: true

check-well-formatted:
runs-on: ubuntu-latest

strategy:
matrix:
eslint: ["true", "false"]
biome: ["true", "false"]

name: "Build and Start T3 App"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check valid matrix
id: matrix-valid
run: |
echo "continue=${{ (matrix.eslint == 'false' || matrix.biome == 'false') && (matrix.biome == 'true' || matrix.eslint == 'true') }}" >> $GITHUB_OUTPUT

- uses: ./.github/actions/setup
if: ${{ steps.matrix-valid.outputs.continue == 'true' }}

- run: pnpm turbo --filter=create-t3-app build
if: ${{ steps.matrix-valid.outputs.continue == 'true' }}

- run: cd cli && pnpm start ../../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} --noGit --CI --trpc --tailwind --nextAuth --drizzle --appRouter --dbProvider=postgres --eslint=${{ matrix.eslint }} --biome=${{ matrix.biome }}
if: ${{ steps.matrix-valid.outputs.continue == 'true' }}

- run: cd ../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} && pnpm build
if: ${{ steps.matrix-valid.outputs.continue == 'true' }}
env:
AUTH_SECRET: foo
AUTH_DISCORD_ID: bar
AUTH_DISCORD_SECRET: baz
SKIP_ENV_VALIDATION: true

# Run biome check
- run: cd ../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} && pnpm check
if: ${{ steps.matrix-valid.outputs.continue == 'true' && matrix.biome == 'true' }}

# Check linting and formatting with eslint and prettier
- run: cd ../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} && pnpm lint && pnpm format:check
if: ${{ steps.matrix-valid.outputs.continue == 'true' && matrix.eslint == 'true' }}
env:
AUTH_SECRET: foo
AUTH_DISCORD_ID: bar
AUTH_DISCORD_SECRET: baz
SKIP_ENV_VALIDATION: true

build-t3-app-with-bun:
runs-on: ubuntu-latest

Expand All @@ -89,7 +139,7 @@ jobs:

- name: We should have a Bun lockfile
run: |
if [ ! -f "../ci-bun/bun.lockb" ]; then
if [ ! -f "../ci-bun/bun.lock" ]; then
echo "Bun lockfile not found"
exit 1
fi
Expand Down
37 changes: 36 additions & 1 deletion 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 @@ -48,7 +52,7 @@ interface CliResults {

const defaultOptions: CliResults = {
appName: DEFAULT_APP_NAME,
packages: ["nextAuth", "prisma", "tailwind", "trpc"],
packages: ["nextAuth", "prisma", "tailwind", "trpc", "eslint"],
flags: {
noGit: false,
noInstall: false,
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
33 changes: 33 additions & 0 deletions cli/src/helpers/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import chalk from "chalk";
import { execa } from "execa";
import ora from "ora";

import { type PackageManager } from "~/utils/getUserPkgManager.js";
import { logger } from "~/utils/logger.js";

// Runs format and lint command to ensure created repository is tidy upon creation
export const formatProject = async ({
pkgManager,
projectDir,
eslint,
biome,
}: {
pkgManager: PackageManager;
projectDir: string;
eslint: boolean;
biome: boolean;
}) => {
logger.info(`Formatting project with ${eslint ? "eslint" : "biome"}...`);
const spinner = ora("Running format command\n").start();

if (eslint) {
await execa(pkgManager, ["format:write"], {
cwd: projectDir,
});
} else if (biome) {
await execa(pkgManager, ["check:unsafe"], {
aidansunbury marked this conversation as resolved.
Show resolved Hide resolved
cwd: projectDir,
});
}
spinner.succeed(`${chalk.green("Successfully formatted project")}`);
};
8 changes: 8 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getUserPkgManager } from "~/utils/getUserPkgManager.js";
import { logger } from "~/utils/logger.js";
import { parseNameAndPath } from "~/utils/parseNameAndPath.js";
import { renderTitle } from "~/utils/renderTitle.js";
import { formatProject } from "./helpers/format.js";
import { installDependencies } from "./helpers/installDependencies.js";
import { getVersion } from "./utils/getT3Version.js";
import {
Expand Down Expand Up @@ -85,6 +86,13 @@ const main = async () => {
await installDependencies({ projectDir });
}

await formatProject({
pkgManager,
projectDir,
eslint: packages.includes("eslint"),
biome: packages.includes("biome"),
});

if (!noGit) {
await initializeGit(projectDir);
}
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: {
"check:unsafe": "biome check --write --unsafe .",
"check:write": "biome check --write .",
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,
});
};
Loading