diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ad5e089cba..fbc4663aa4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,8 +27,9 @@ jobs: tailwind: ["true", "false"] nextAuth: ["true", "false"] prisma: ["true", "false"] + prettierAndExtendedEslint: ["true", "false"] - name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}" + name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.prettierAndExtendedEslint }}" steps: - uses: actions/checkout@v3 with: @@ -65,7 +66,7 @@ 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 }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} - - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }} && pnpm build + - run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.prettierAndExtendedEslint }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --prettier-and-extended-eslint=${{ matrix.prettierAndExtendedEslint }} + - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.prettierAndExtendedEslint }} && pnpm build env: NEXTAUTH_SECRET: foo diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index 73c8833879..bc7e3b8e71 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -2,8 +2,10 @@ import chalk from "chalk"; import { Command } from "commander"; import inquirer from "inquirer"; import { CREATE_T3_APP, DEFAULT_APP_NAME } from "~/consts.js"; -import { type AvailablePackages } from "~/installers/index.js"; -import { availablePackages } from "~/installers/index.js"; +import { + availablePackages, + type AvailablePackages, +} from "~/installers/index.js"; import { getVersion } from "~/utils/getT3Version.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; import { logger } from "~/utils/logger.js"; @@ -15,6 +17,7 @@ interface CliFlags { noInstall: boolean; default: boolean; importAlias: string; + prettierAndExtendedEslint: boolean; /** @internal Used in CI. */ CI: boolean; @@ -47,6 +50,7 @@ const defaultOptions: CliResults = { prisma: false, nextAuth: false, importAlias: "~/", + prettierAndExtendedEslint: false, }, }; @@ -114,6 +118,12 @@ export const runCli = async () => { "Explicitly tell the CLI to use a custom import alias", defaultOptions.flags.importAlias, ) + /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ + .option( + "-p, --prettier-and-extended-eslint", + "Explicitly tell the CLI to use a custom import alias", + defaultOptions.flags.prettierAndExtendedEslint, + ) /** END CI-FLAGS */ .version(getVersion(), "-v, --version", "Display the version number") .addHelpText( @@ -153,6 +163,9 @@ export const runCli = async () => { if (cliResults.flags.tailwind) cliResults.packages.push("tailwind"); if (cliResults.flags.prisma) cliResults.packages.push("prisma"); if (cliResults.flags.nextAuth) cliResults.packages.push("nextAuth"); + if (cliResults.flags.prettierAndExtendedEslint) { + cliResults.flags.prettierAndExtendedEslint = true; + } } // Explained below why this is in a try/catch block @@ -186,6 +199,9 @@ export const runCli = async () => { } cliResults.flags.importAlias = await promptImportAlias(); + + cliResults.flags.prettierAndExtendedEslint = + await promtPrettierAndExtendedEslint(); } } catch (err) { // If the user is not calling create-t3-app from an interactive terminal, inquirer will throw an error with isTTYError = true @@ -329,3 +345,16 @@ const promptImportAlias = async (): Promise => { return importAlias; }; + +const promtPrettierAndExtendedEslint = async (): Promise => { + const { prettierAndExtendedEslint } = await inquirer.prompt< + Pick + >({ + name: "prettierAndExtendedEslint", + type: "confirm", + message: "Would you like to use prettier and extended eslint conifg?", + default: defaultOptions.flags.prettierAndExtendedEslint, + }); + + return prettierAndExtendedEslint; +}; diff --git a/cli/src/helpers/createProject.ts b/cli/src/helpers/createProject.ts index ea1e3942f4..fe2ca1560c 100644 --- a/cli/src/helpers/createProject.ts +++ b/cli/src/helpers/createProject.ts @@ -2,7 +2,9 @@ import path from "path"; import { installPackages } from "~/helpers/installPackages.js"; import { scaffoldProject } from "~/helpers/scaffoldProject.js"; import { selectAppFile, selectIndexFile } from "~/helpers/selectBoilerplate.js"; +import { installExtendedEslint } from "~/installers/eslint.js"; import { type PkgInstallerMap } from "~/installers/index.js"; +import { installPrettier } from "~/installers/prettier.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; interface CreateProjectOptions { @@ -10,12 +12,14 @@ interface CreateProjectOptions { packages: PkgInstallerMap; noInstall: boolean; importAlias: string; + prettierAndExtendedEslint: boolean; } export const createProject = async ({ projectName, packages, noInstall, + prettierAndExtendedEslint, }: CreateProjectOptions) => { const pkgManager = getUserPkgManager(); const projectDir = path.resolve(process.cwd(), projectName); @@ -36,6 +40,16 @@ export const createProject = async ({ noInstall, }); + if (prettierAndExtendedEslint) { + const installArgs = { + projectDir, + pkgManager, + noInstall, + }; + installPrettier(installArgs); + installExtendedEslint(installArgs); + } + // TODO: Look into using handlebars or other templating engine to scaffold without needing to maintain multiple copies of the same file selectAppFile({ projectDir, packages }); selectIndexFile({ projectDir, packages }); diff --git a/cli/src/index.ts b/cli/src/index.ts index 66bca71a87..3e867b6e88 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -32,7 +32,7 @@ const main = async () => { const { appName, packages, - flags: { noGit, noInstall, importAlias }, + flags: { noGit, noInstall, importAlias, prettierAndExtendedEslint }, } = await runCli(); const usePackages = buildPkgInstallerMap(packages); @@ -45,6 +45,7 @@ const main = async () => { packages: usePackages, importAlias: importAlias, noInstall, + prettierAndExtendedEslint, }); // Write name to package.json @@ -67,10 +68,12 @@ const main = async () => { } // Rename _eslintrc.json to .eslintrc.json - we use _eslintrc.json to avoid conflicts with the monorepos linter - fs.renameSync( - path.join(projectDir, "_eslintrc.cjs"), - path.join(projectDir, ".eslintrc.cjs"), - ); + if (fs.existsSync(path.join(projectDir, "_eslintrc.cjs"))) { + fs.renameSync( + path.join(projectDir, "_eslintrc.cjs"), + path.join(projectDir, ".eslintrc.cjs"), + ); + } if (!noGit) { await initializeGit(projectDir); diff --git a/cli/src/installers/dependencyVersionMap.ts b/cli/src/installers/dependencyVersionMap.ts index dffeacc809..5fde931227 100644 --- a/cli/src/installers/dependencyVersionMap.ts +++ b/cli/src/installers/dependencyVersionMap.ts @@ -15,9 +15,6 @@ export const dependencyVersionMap = { tailwindcss: "^3.3.0", autoprefixer: "^10.4.14", postcss: "^8.4.21", - prettier: "^2.8.6", - "prettier-plugin-tailwindcss": "^0.2.6", - "@types/prettier": "^2.7.2", // tRPC "@trpc/client": "^10.18.0", @@ -26,5 +23,14 @@ export const dependencyVersionMap = { "@trpc/next": "^10.18.0", "@tanstack/react-query": "^4.28.0", superjson: "1.12.2", + + // Prettier + prettier: "^2.8.6", + "prettier-plugin-tailwindcss": "^0.2.6", + "@types/prettier": "^2.7.2", + "@ianvs/prettier-plugin-sort-imports": "^3.7.2", + + // Eslint + "eslint-config-next": "^13.4.1", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/cli/src/installers/eslint.ts b/cli/src/installers/eslint.ts new file mode 100644 index 0000000000..7c5536132b --- /dev/null +++ b/cli/src/installers/eslint.ts @@ -0,0 +1,23 @@ +import { type Installer } from "./index.js"; +import fs from "fs-extra"; +import path from "path"; +import { PKG_ROOT } from "~/consts.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; + +export const installExtendedEslint: Installer = ({ projectDir }) => { + addPackageDependency({ + dependencies: ["eslint-config-next"], + devMode: true, + projectDir, + }); + + const configDir = path.join(PKG_ROOT, "template/extras/config"); + + // Remove the default config + fs.rmSync(path.join(projectDir, "_eslintrc.cjs")); + + fs.copySync( + path.join(configDir, "_eslintrc.cjs"), + path.join(projectDir, ".eslintrc.cjs"), + ); +}; diff --git a/cli/src/installers/prettier.ts b/cli/src/installers/prettier.ts new file mode 100644 index 0000000000..8b33d3b9d0 --- /dev/null +++ b/cli/src/installers/prettier.ts @@ -0,0 +1,55 @@ +import { type Installer } from "../installers/index.js"; +import { type AvailableDependencies } from "./dependencyVersionMap.js"; +import fs from "fs-extra"; +import path from "path"; +import { PKG_ROOT } from "~/consts.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { addPackageScript } from "~/utils/addPackageScript.js"; + +export const installPrettier: Installer = ({ projectDir, packages }) => { + const packeagesToInstall: AvailableDependencies[] = [ + "prettier", + "@types/prettier", + "@ianvs/prettier-plugin-sort-imports", + ]; + + if (packages?.tailwind.inUse) { + packeagesToInstall.push("prettier-plugin-tailwindcss"); + } + + addPackageDependency({ + projectDir, + dependencies: packeagesToInstall, + devMode: true, + }); + + addPackageScript({ + projectDir, + scripts: [ + { + name: "format", + value: "pnpm format:check --write", + }, + { + name: "format:check", + value: + "pnpm prettier --check --plugin-search-dir=. **/*.{cjs,mjs,ts,tsx,md,json} --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern", + }, + ], + }); + + if (packages?.tailwind.inUse) { + fs.copySync( + path.join( + PKG_ROOT, + "template/extras/config/prettier-with-tailwind.config.cjs", + ), + path.join(projectDir, "prettier.config.cjs"), + ); + } else { + fs.copySync( + path.join(PKG_ROOT, "template/extras/config/_prettier.config.cjs"), + path.join(projectDir, "prettier.config.cjs"), + ); + } +}; diff --git a/cli/src/installers/tailwind.ts b/cli/src/installers/tailwind.ts index 10fd860a3c..681841b989 100644 --- a/cli/src/installers/tailwind.ts +++ b/cli/src/installers/tailwind.ts @@ -7,14 +7,7 @@ import { addPackageDependency } from "~/utils/addPackageDependency.js"; export const tailwindInstaller: Installer = ({ projectDir }) => { addPackageDependency({ projectDir, - dependencies: [ - "tailwindcss", - "postcss", - "autoprefixer", - "prettier", - "prettier-plugin-tailwindcss", - "@types/prettier", - ], + dependencies: ["tailwindcss", "postcss", "autoprefixer"], devMode: true, }); @@ -26,16 +19,12 @@ export const tailwindInstaller: Installer = ({ projectDir }) => { const postcssCfgSrc = path.join(extrasDir, "config/postcss.config.cjs"); const postcssCfgDest = path.join(projectDir, "postcss.config.cjs"); - const prettierSrc = path.join(extrasDir, "config/prettier.config.cjs"); - const prettierDest = path.join(projectDir, "prettier.config.cjs"); - const cssSrc = path.join(extrasDir, "src/styles/globals.css"); const cssDest = path.join(projectDir, "src/styles/globals.css"); fs.copySync(twCfgSrc, twCfgDest); fs.copySync(postcssCfgSrc, postcssCfgDest); fs.copySync(cssSrc, cssDest); - fs.copySync(prettierSrc, prettierDest); // Remove vanilla css file const indexModuleCss = path.join(projectDir, "src/pages/index.module.css"); diff --git a/cli/src/utils/addPackageScript.ts b/cli/src/utils/addPackageScript.ts new file mode 100644 index 0000000000..6ad54b3e44 --- /dev/null +++ b/cli/src/utils/addPackageScript.ts @@ -0,0 +1,26 @@ +import fs from "fs-extra"; +import path from "path"; +import sortPackageJson from "sort-package-json"; +import { type PackageJson } from "type-fest"; + +export const addPackageScript = (opts: { + scripts: { name: string; value: string }[]; + projectDir: string; +}) => { + const pkgJson = fs.readJSONSync( + path.join(opts.projectDir, "package.json"), + ) as PackageJson; + + if (!pkgJson.scripts) { + pkgJson.scripts = {}; + } + + for (const script of opts.scripts) { + pkgJson.scripts[script.name] = script.value; + } + const sortedPkgJson = sortPackageJson(pkgJson); + + fs.writeJSONSync(path.join(opts.projectDir, "package.json"), sortedPkgJson, { + spaces: 2, + }); +}; diff --git a/cli/template/base/_eslintrc.cjs b/cli/template/base/_eslintrc.cjs index 8b7a0e8f9c..3733acb0cf 100644 --- a/cli/template/base/_eslintrc.cjs +++ b/cli/template/base/_eslintrc.cjs @@ -3,33 +3,11 @@ const path = require("path"); /** @type {import("eslint").Linter.Config} */ const config = { - overrides: [ - { - extends: [ - "plugin:@typescript-eslint/recommended-requiring-type-checking", - ], - files: ["*.ts", "*.tsx"], - parserOptions: { - project: path.join(__dirname, "tsconfig.json"), - }, - }, - ], parser: "@typescript-eslint/parser", parserOptions: { project: path.join(__dirname, "tsconfig.json"), }, plugins: ["@typescript-eslint"], - extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], - rules: { - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - prefer: "type-imports", - fixStyle: "inline-type-imports", - }, - ], - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - }, }; module.exports = config; diff --git a/cli/template/base/package.json b/cli/template/base/package.json index 23bb2c89da..104332ac5c 100644 --- a/cli/template/base/package.json +++ b/cli/template/base/package.json @@ -23,7 +23,6 @@ "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "eslint": "^8.36.0", - "eslint-config-next": "^13.4.1", "typescript": "^5.0.2" } } diff --git a/cli/template/extras/config/_eslintrc.cjs b/cli/template/extras/config/_eslintrc.cjs new file mode 100644 index 0000000000..758c95a7b3 --- /dev/null +++ b/cli/template/extras/config/_eslintrc.cjs @@ -0,0 +1,43 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require("path"); + +/** @type {import("eslint").Linter.Config} */ +const config = { + overrides: [ + { + extends: [ + "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + files: ["*.ts", "*.tsx"], + parserOptions: { + project: path.join(__dirname, "tsconfig.json"), + }, + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + project: path.join(__dirname, "tsconfig.json"), + }, + plugins: ["@typescript-eslint"], + extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], + rules: { + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + prefer: "type-imports", + fixStyle: "inline-type-imports", + }, + ], + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-misused-promises": [ + 2, + { + checksVoidReturn: { + attributes: false, + }, + }, + ], + }, +}; + +module.exports = config; diff --git a/cli/template/extras/config/prettier.config.cjs b/cli/template/extras/config/_prettier.config.cjs similarity index 58% rename from cli/template/extras/config/prettier.config.cjs rename to cli/template/extras/config/_prettier.config.cjs index ca28ed9e46..6058b6de93 100644 --- a/cli/template/extras/config/prettier.config.cjs +++ b/cli/template/extras/config/_prettier.config.cjs @@ -1,6 +1,6 @@ /** @type {import("prettier").Config} */ const config = { - plugins: [require.resolve("prettier-plugin-tailwindcss")], + plugins: ["@ianvs/prettier-plugin-sort-imports"], }; module.exports = config; diff --git a/cli/template/extras/config/prettier-with-tailwind.config.cjs b/cli/template/extras/config/prettier-with-tailwind.config.cjs new file mode 100644 index 0000000000..f817bf3608 --- /dev/null +++ b/cli/template/extras/config/prettier-with-tailwind.config.cjs @@ -0,0 +1,9 @@ +/** @type {import("prettier").Config} */ +const config = { + plugins: [ + "@ianvs/prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss", + ], +}; + +module.exports = config; diff --git a/www/.prettierrc.cjs b/www/.prettierrc.cjs index 3dd61cf5bd..39a5ef01c8 100644 --- a/www/.prettierrc.cjs +++ b/www/.prettierrc.cjs @@ -5,6 +5,7 @@ const config = { plugins: [ ...baseConfig.plugins, require.resolve("prettier-plugin-astro"), + require.resolve("@ianvs/prettier-plugin-sort-imports"), require.resolve("prettier-plugin-tailwindcss"), // MUST come last ], pluginSearchDirs: false, diff --git a/www/src/pages/en/usage/tailwind.md b/www/src/pages/en/usage/tailwind.md index bdb355a4b0..12e11a3521 100644 --- a/www/src/pages/en/usage/tailwind.md +++ b/www/src/pages/en/usage/tailwind.md @@ -74,7 +74,7 @@ Make sure you have editor plugins for Tailwind installed to improve your experie ### Formatting -Tailwind CSS classes can easily get a bit messy, so a formatter for the classes is a must have. [Tailwind CSS Prettier Plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) sorts the classes in the [recommended order](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted) so that the classes match the outputted css bundle. When selecting Tailwind in the CLI, we will install and configure this for you. +Tailwind CSS classes can easily get a bit messy, so a formatter for the classes is a must have. [Tailwind CSS Prettier Plugin](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) sorts the classes in the [recommended order](https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted) so that the classes match the outputted css bundle. When selecting Tailwind in the CLI, we will install and configure this for you when the prettier option is selected. ### Conditionally Applying Classes