|
| 1 | +import fs from 'fs-extra'; |
| 2 | +import path from 'node:path'; |
| 3 | +import { applyTemplate, type TemplateConfiguration } from '../template'; |
| 4 | +import sortObjectKeys from './sortObjectKeys'; |
| 5 | + |
| 6 | +type Tool = { |
| 7 | + name: string; |
| 8 | + description: string; |
| 9 | + package: Record<string, unknown>; |
| 10 | + condition?: (config: TemplateConfiguration) => boolean; |
| 11 | +}; |
| 12 | + |
| 13 | +type Options = { |
| 14 | + tools: string[]; |
| 15 | + root: string; |
| 16 | + packageJson: Record<string, unknown>; |
| 17 | + config: TemplateConfiguration; |
| 18 | +}; |
| 19 | + |
| 20 | +const ESLINT = { |
| 21 | + name: 'ESLint with Prettier', |
| 22 | + description: 'Lint and format code', |
| 23 | + package: { |
| 24 | + scripts: { |
| 25 | + lint: 'eslint "**/*.{js,ts,tsx}"', |
| 26 | + }, |
| 27 | + prettier: { |
| 28 | + quoteProps: 'consistent', |
| 29 | + singleQuote: true, |
| 30 | + tabWidth: 2, |
| 31 | + trailingComma: 'es5', |
| 32 | + useTabs: false, |
| 33 | + }, |
| 34 | + devDependencies: { |
| 35 | + '@eslint/compat': '^1.3.2', |
| 36 | + '@eslint/eslintrc': '^3.3.1', |
| 37 | + '@eslint/js': '^9.35.0', |
| 38 | + '@react-native/eslint-config': '^0.81.1', |
| 39 | + 'eslint-config-prettier': '^10.1.8', |
| 40 | + 'eslint-plugin-prettier': '^5.5.4', |
| 41 | + 'eslint': '^9.35.0', |
| 42 | + 'prettier': '^2.8.8', |
| 43 | + }, |
| 44 | + }, |
| 45 | +}; |
| 46 | + |
| 47 | +const LEFTHOOK = { |
| 48 | + name: 'Lefthook with Commitlint', |
| 49 | + description: 'Manage Git hooks and lint commit messages', |
| 50 | + package: { |
| 51 | + commitlint: { |
| 52 | + extends: ['@commitlint/config-conventional'], |
| 53 | + }, |
| 54 | + devDependencies: { |
| 55 | + '@commitlint/config-conventional': '^19.8.1', |
| 56 | + 'commitlint': '^19.8.1', |
| 57 | + 'lefthook': '^2.0.3', |
| 58 | + }, |
| 59 | + }, |
| 60 | +}; |
| 61 | + |
| 62 | +const RELEASE_IT = { |
| 63 | + name: 'Release It', |
| 64 | + description: 'Automate versioning and package publishing tasks', |
| 65 | + package: { |
| 66 | + 'scripts': { |
| 67 | + release: 'release-it --only-version', |
| 68 | + }, |
| 69 | + 'release-it': { |
| 70 | + git: { |
| 71 | + // eslint-disable-next-line no-template-curly-in-string |
| 72 | + commitMessage: 'chore: release ${version}', |
| 73 | + // eslint-disable-next-line no-template-curly-in-string |
| 74 | + tagName: 'v${version}', |
| 75 | + }, |
| 76 | + npm: { |
| 77 | + publish: true, |
| 78 | + }, |
| 79 | + github: { |
| 80 | + release: true, |
| 81 | + }, |
| 82 | + plugins: { |
| 83 | + '@release-it/conventional-changelog': { |
| 84 | + preset: { |
| 85 | + name: 'angular', |
| 86 | + }, |
| 87 | + }, |
| 88 | + }, |
| 89 | + }, |
| 90 | + 'devDependencies': { |
| 91 | + 'release-it': '^19.0.4', |
| 92 | + '@release-it/conventional-changelog': '^10.0.1', |
| 93 | + }, |
| 94 | + }, |
| 95 | +}; |
| 96 | + |
| 97 | +const JEST = { |
| 98 | + name: 'Jest', |
| 99 | + description: 'Test JavaScript and TypeScript code', |
| 100 | + package: { |
| 101 | + scripts: { |
| 102 | + test: 'jest', |
| 103 | + }, |
| 104 | + jest: { |
| 105 | + preset: 'react-native', |
| 106 | + modulePathIgnorePatterns: [ |
| 107 | + '<rootDir>/example/node_modules', |
| 108 | + '<rootDir>/lib/', |
| 109 | + ], |
| 110 | + }, |
| 111 | + devDependencies: { |
| 112 | + '@types/jest': '^29.5.14', |
| 113 | + 'jest': '^29.7.0', |
| 114 | + }, |
| 115 | + }, |
| 116 | +}; |
| 117 | + |
| 118 | +const TURBOREPO = { |
| 119 | + name: 'Turborepo', |
| 120 | + description: 'Cache build outputs on CI', |
| 121 | + package: { |
| 122 | + devDependencies: { |
| 123 | + turbo: '^2.5.6', |
| 124 | + }, |
| 125 | + }, |
| 126 | + condition: (config: TemplateConfiguration) => config.example !== 'expo', |
| 127 | +}; |
| 128 | + |
| 129 | +export const AVAILABLE_TOOLS = { |
| 130 | + 'eslint': ESLINT, |
| 131 | + 'lefthook': LEFTHOOK, |
| 132 | + 'release-it': RELEASE_IT, |
| 133 | + 'jest': JEST, |
| 134 | +} as const satisfies Record<string, Tool>; |
| 135 | + |
| 136 | +const REQUIRED_TOOLS = { |
| 137 | + turbo: TURBOREPO, |
| 138 | +} as const satisfies Record<string, Tool>; |
| 139 | + |
| 140 | +const ALL_TOOLS = { |
| 141 | + ...AVAILABLE_TOOLS, |
| 142 | + ...REQUIRED_TOOLS, |
| 143 | +} as const; |
| 144 | + |
| 145 | +export async function configureTools({ |
| 146 | + tools, |
| 147 | + config, |
| 148 | + root, |
| 149 | + packageJson, |
| 150 | +}: Options) { |
| 151 | + for (const key of [ |
| 152 | + ...tools, |
| 153 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion |
| 154 | + ...(Object.keys(REQUIRED_TOOLS) as (keyof typeof REQUIRED_TOOLS)[]), |
| 155 | + ]) { |
| 156 | + if (!(key in ALL_TOOLS)) { |
| 157 | + throw new Error( |
| 158 | + `Invalid tool '${key}'. Available tools are: ${Object.keys( |
| 159 | + AVAILABLE_TOOLS |
| 160 | + ).join(', ')}.` |
| 161 | + ); |
| 162 | + } |
| 163 | + |
| 164 | + // @ts-expect-error: We checked the key above |
| 165 | + const tool: Tool = ALL_TOOLS[key]; |
| 166 | + |
| 167 | + if (tool.condition && !tool.condition(config)) { |
| 168 | + continue; |
| 169 | + } |
| 170 | + |
| 171 | + const files = path.resolve(__dirname, `../../templates/tools/${key}`); |
| 172 | + |
| 173 | + if (fs.existsSync(files)) { |
| 174 | + await applyTemplate(config, files, root); |
| 175 | + } |
| 176 | + |
| 177 | + for (const [key, value] of Object.entries(tool.package)) { |
| 178 | + if ( |
| 179 | + typeof value === 'object' && |
| 180 | + value !== null && |
| 181 | + !Array.isArray(value) |
| 182 | + ) { |
| 183 | + if (typeof packageJson[key] === 'object' || packageJson[key] == null) { |
| 184 | + packageJson[key] = { |
| 185 | + ...packageJson[key], |
| 186 | + ...value, |
| 187 | + }; |
| 188 | + |
| 189 | + if ( |
| 190 | + key === 'dependencies' || |
| 191 | + key === 'devDependencies' || |
| 192 | + key === 'peerDependencies' |
| 193 | + ) { |
| 194 | + // @ts-expect-error: We know they are objects here |
| 195 | + packageJson[key] = sortObjectKeys(packageJson[key]); |
| 196 | + } |
| 197 | + } else { |
| 198 | + throw new Error( |
| 199 | + `Cannot merge '${key}' field because it is not an object (got '${String(packageJson[key])}').` |
| 200 | + ); |
| 201 | + } |
| 202 | + } else { |
| 203 | + packageJson[key] = value; |
| 204 | + } |
| 205 | + } |
| 206 | + } |
| 207 | +} |
0 commit comments