Skip to content
28 changes: 27 additions & 1 deletion apps/cli/src/__meta__.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export const META: Meta = {
},
},
linter: {
prompt: 'Choose a linter?',
prompt: 'Code quality tools?',
selection: 'single',
options: {
biome: {
Expand All @@ -401,6 +401,17 @@ export const META: Meta = {
},
},
},
'eslint-prettier': {
label: 'ESLint + Prettier',
hint: 'Lint with ESLint, format with Prettier',
compose: ['eslint', 'prettier'],
mono: { scope: 'pkg', name: 'eslint-config' },
packageJson: {
devDependencies: {
'eslint-config-prettier': '^10.1.8',
},
},
},
eslint: {
label: 'ESLint',
hint: 'Most popular JavaScript linter',
Expand Down Expand Up @@ -429,6 +440,21 @@ export const META: Meta = {
},
},
},
prettier: {
label: 'Prettier',
hint: 'Opinionated code formatter (no linter)',
mono: { scope: 'root' },
packageJson: {
devDependencies: {
prettier: '^3.8.1',
'prettier-plugin-tailwindcss': '^0.7.2',
},
scripts: {
format: 'prettier --write .',
'format:check': 'prettier --check .',
},
},
},
},
},
tooling: {
Expand Down
68 changes: 47 additions & 21 deletions apps/cli/src/lib/package-json-generator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execSync } from 'node:child_process';
import { META } from '@/__meta__';
import { META, type ProjectCategoryName } from '@/__meta__';
import { isLibraryCompatible } from '@/lib/addon-utils';
import type { AppContext, PackageManager, TemplateContext } from '@/types/ctx';
import type { MetaAddon, PackageJsonConfig } from '@/types/meta';
Expand Down Expand Up @@ -84,6 +84,23 @@ function getProjectAddonPackageName(addon: MetaAddon): string | null {
return null;
}

function resolveCompositeAddons(
category: ProjectCategoryName,
addonName: string,
): { name: string; addon: MetaAddon }[] {
const addon = META.project[category].options[addonName];
if (!addon) return [];
if (!addon.compose) return [{ name: addonName, addon }];

const parts: { name: string; addon: MetaAddon }[] = [];
for (const name of addon.compose) {
const composed = META.project[category].options[name];
if (composed) parts.push({ name, addon: composed });
}
parts.push({ name: addonName, addon });
return parts;
}

function stripInternalDeps(deps: Record<string, string>): Record<string, string> {
return Object.fromEntries(Object.entries(deps).filter(([key]) => !key.startsWith('@repo/')));
}
Expand Down Expand Up @@ -167,21 +184,21 @@ export function generateAppPackageJson(app: AppContext, ctx: TemplateContext, ap
}
}

// Process linter addon
// Process linter addon (with composite expansion)
if (ctx.project.linter) {
const linterAddon = META.project.linter.options[ctx.project.linter];
if (linterAddon) {
const packageName = getProjectAddonPackageName(linterAddon);
const parts = resolveCompositeAddons('linter', ctx.project.linter);
for (const { addon } of parts) {
const packageName = getProjectAddonPackageName(addon);
if (packageName && isTurborepo) {
merged.devDependencies = {
...merged.devDependencies,
[`@repo/${packageName}`]: '*',
};
if (linterAddon.appPackageJson) {
merged = mergePackageJsonConfigs(merged, linterAddon.appPackageJson);
if (addon.appPackageJson) {
merged = mergePackageJsonConfigs(merged, addon.appPackageJson);
}
} else if (!isTurborepo) {
merged = mergePackageJsonConfigs(merged, linterAddon.packageJson, linterAddon.appPackageJson);
merged = mergePackageJsonConfigs(merged, addon.packageJson, addon.appPackageJson);
}
}
}
Expand Down Expand Up @@ -286,15 +303,17 @@ export function generateRootPackageJson(ctx: TemplateContext): GeneratedPackageJ
}
}

// Add root-scoped linter to root package.json (biome)
// Add root-scoped linter deps to root package.json (with composite expansion)
if (ctx.project.linter) {
const linterAddon = META.project.linter.options[ctx.project.linter];
if (linterAddon?.mono?.scope === 'root' && linterAddon.packageJson) {
if (linterAddon.packageJson.devDependencies) {
devDependencies = { ...devDependencies, ...linterAddon.packageJson.devDependencies };
}
if (linterAddon.packageJson.scripts) {
Object.assign(scripts, linterAddon.packageJson.scripts);
const parts = resolveCompositeAddons('linter', ctx.project.linter);
for (const { addon } of parts) {
if (addon.mono?.scope === 'root' && addon.packageJson) {
if (addon.packageJson.devDependencies) {
devDependencies = { ...devDependencies, ...addon.packageJson.devDependencies };
}
if (addon.packageJson.scripts) {
Object.assign(scripts, addon.packageJson.scripts);
}
}
}
}
Expand Down Expand Up @@ -338,12 +357,19 @@ export function generateAllPackageJsons(ctx: TemplateContext): GeneratedPackageJ
}
}

// Collect linter package (eslint-config)
// Collect linter packages (with composite expansion)
if (ctx.project.linter) {
const linterAddon = META.project.linter.options[ctx.project.linter];
if (linterAddon?.mono?.scope === 'pkg') {
const pkgName = linterAddon.mono.name;
extractedPackages.set(pkgName, linterAddon.packageJson ?? {});
const parts = resolveCompositeAddons('linter', ctx.project.linter);
for (const { addon } of parts) {
if (addon.mono?.scope === 'pkg') {
const pkgName = addon.mono.name;
const existing = extractedPackages.get(pkgName);
if (existing) {
extractedPackages.set(pkgName, mergePackageJsonConfigs(existing, addon.packageJson ?? {}));
} else {
extractedPackages.set(pkgName, addon.packageJson ?? {});
}
}
}
}

Expand Down
13 changes: 11 additions & 2 deletions apps/cli/src/lib/template-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { parseStackSuffix, readFrontmatterFile, shouldSkipTemplate } from './fro

const VALID_STACKS = Object.keys(META.stacks);

export function resolveAddonNames(category: ProjectCategoryName, addonName: string): string[] {
const addon = META.project[category].options[addonName];
if (addon?.compose) return addon.compose;
return [addonName];
}

export function resolveLibraryDestination(
relativePath: string,
library: MetaAddon,
Expand Down Expand Up @@ -226,8 +232,11 @@ export function getAllTemplatesForContext(ctx: TemplateContext): TemplateFile[]
templates.push(...resolveTemplatesForProjectAddon('orm', ctx.project.orm, ctx));
}
if (ctx.project.linter) {
templates.push(...resolveTemplatesForProjectAddon('linter', ctx.project.linter, ctx));
templates.push(...resolveStackSpecificAddonTemplatesForApps('linter', ctx.project.linter, ctx.apps, ctx));
const addonNames = resolveAddonNames('linter', ctx.project.linter);
for (const name of addonNames) {
templates.push(...resolveTemplatesForProjectAddon('linter', name, ctx));
templates.push(...resolveStackSpecificAddonTemplatesForApps('linter', name, ctx.apps, ctx));
}
}
for (const tooling of ctx.project.tooling) {
templates.push(...resolveTemplatesForProjectAddon('tooling', tooling, ctx));
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/types/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface MetaAddon {
label: string;
hint?: string;
category?: string;
compose?: string[];
support?: AddonSupport;
require?: AddonRequire;
mono?: AddonMono;
Expand Down
6 changes: 6 additions & 0 deletions apps/cli/templates/project/linter/eslint/base.js.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ mono:
---
import js from "@eslint/js";
import tseslint from "typescript-eslint";
{{#if (has 'linter' 'eslint-prettier')}}
import eslintConfigPrettier from "eslint-config-prettier/flat";
{{/if}}

export const baseConfig = [
js.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist/**", "node_modules/**"],
},
{{#if (has 'linter' 'eslint-prettier')}}
eslintConfigPrettier,
{{/if}}
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
{{#if (has 'linter' 'eslint-prettier')}}
import eslintConfigPrettier from "eslint-config-prettier/flat";
{{/if}}

export default defineConfig([
js.configs.recommended,
Expand Down Expand Up @@ -37,5 +40,8 @@ export default defineConfig([
"react/react-in-jsx-scope": "off",
},
},
{{#if (has 'linter' 'eslint-prettier')}}
eslintConfigPrettier,
{{/if}}
]);
{{/if}}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { defineConfig, globalIgnores } from "eslint/config";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";
{{#if (has 'linter' 'eslint-prettier')}}
import eslintConfigPrettier from "eslint-config-prettier/flat";
{{/if}}

export default defineConfig([
js.configs.recommended,
Expand All @@ -19,5 +22,8 @@ export default defineConfig([
},
},
},
{{#if (has 'linter' 'eslint-prettier')}}
eslintConfigPrettier,
{{/if}}
]);
{{/if}}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginNext from "@next/eslint-plugin-next";
import globals from "globals";
{{#if (has 'linter' 'eslint-prettier')}}
import eslintConfigPrettier from "eslint-config-prettier/flat";
{{/if}}

export default defineConfig([
js.configs.recommended,
Expand Down Expand Up @@ -47,5 +50,8 @@ export default defineConfig([
"react/react-in-jsx-scope": "off",
},
},
{{#if (has 'linter' 'eslint-prettier')}}
eslintConfigPrettier,
{{/if}}
]);
{{/if}}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import pluginReactHooks from "eslint-plugin-react-hooks";
import globals from "globals";
{{#if (has 'linter' 'eslint-prettier')}}
import eslintConfigPrettier from "eslint-config-prettier/flat";
{{/if}}

export default defineConfig([
js.configs.recommended,
Expand Down Expand Up @@ -36,5 +39,8 @@ export default defineConfig([
"react/react-in-jsx-scope": "off",
},
},
{{#if (has 'linter' 'eslint-prettier')}}
eslintConfigPrettier,
{{/if}}
]);
{{/if}}
12 changes: 12 additions & 0 deletions apps/cli/templates/project/linter/prettier/__prettierignore.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
dist
build
out
.next
.turbo
coverage
node_modules
*.min.js
*.min.css
pnpm-lock.yaml
bun.lockb
package-lock.json
9 changes: 9 additions & 0 deletions apps/cli/templates/project/linter/prettier/__prettierrc.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["clsx", "cn"]
}
Loading
Loading