Skip to content

Commit b9f57fc

Browse files
committed
feat: add option to configure tools individually
1 parent eb4b06b commit b9f57fc

File tree

13 files changed

+342
-79
lines changed

13 files changed

+342
-79
lines changed

.github/workflows/build-templates.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686
--type ${{ matrix.type.name }}
8787
--languages ${{ matrix.type.language }}
8888
--example ${{ matrix.type.language == 'js' && 'expo' || 'vanilla' }}
89+
--tools eslint lefthook release-it jest
8990
)
9091
9192
if [[ ${{ github.event_name }} == 'schedule' ]]; then

packages/create-react-native-library/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { determinePackageManager } from './utils/packageManager';
3333
import { prompt } from './utils/prompt';
3434
import { resolveNpmPackageVersion } from './utils/resolveNpmPackageVersion';
3535
import { hideBin } from 'yargs/helpers';
36+
import { configureTools } from './utils/configureTools';
3637

3738
type Args = Partial<Answers> & {
3839
$0: string;
@@ -120,6 +121,17 @@ async function create(_argv: Args) {
120121
await alignDependencyVersionsWithExampleApp(rootPackageJson, folder);
121122
}
122123

124+
if (!answers.local && answers.tools.length > 0) {
125+
spinner.text = 'Configuring tools';
126+
127+
await configureTools({
128+
tools: answers.tools,
129+
config,
130+
root: folder,
131+
packageJson: rootPackageJson,
132+
});
133+
}
134+
123135
const libraryMetadata = createMetadata(answers);
124136

125137
rootPackageJson['create-react-native-library'] = libraryMetadata;

packages/create-react-native-library/src/input.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { version } from '../package.json';
77
import { SUPPORTED_REACT_NATIVE_VERSION } from './constants';
88
import type { Question } from './utils/prompt';
99
import { spawn } from './utils/spawn';
10+
import { AVAILABLE_TOOLS } from './utils/configureTools';
1011

1112
export type ProjectLanguages = 'kotlin-objc' | 'kotlin-swift' | 'js';
1213

@@ -145,6 +146,11 @@ export const acceptedArgs = {
145146
type: 'string',
146147
choices: EXAMPLE_CHOICES.map(({ value }) => value),
147148
},
149+
'tools': {
150+
description: 'Tools to configure',
151+
type: 'array',
152+
choices: Object.keys(AVAILABLE_TOOLS),
153+
},
148154
'interactive': {
149155
description: 'Whether to run in interactive mode',
150156
type: 'boolean',
@@ -164,6 +170,7 @@ type PromptAnswers = {
164170
languages: ProjectLanguages;
165171
type: ProjectType;
166172
example: ExampleApp;
173+
tools: string[];
167174
local: boolean;
168175
};
169176

@@ -386,6 +393,17 @@ export async function createQuestions({
386393
});
387394
},
388395
},
396+
{
397+
type: (_, answers) => ((answers.local ?? local) ? null : 'multiselect'),
398+
name: 'tools',
399+
message: 'Which tools do you want to configure?',
400+
choices: Object.entries(AVAILABLE_TOOLS).map(([key, tool]) => ({
401+
value: key,
402+
title: tool.name,
403+
description: tool.description,
404+
selected: true,
405+
})),
406+
},
389407
];
390408

391409
return questions;

packages/create-react-native-library/src/template.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type TemplateConfiguration = {
4242
repo: string;
4343
example: ExampleApp;
4444
year: number;
45+
tools: string[];
4546
};
4647

4748
const BINARIES = [
@@ -144,6 +145,7 @@ export function generateTemplateConfiguration({
144145
},
145146
repo: answers.repoUrl,
146147
example: answers.example,
148+
tools: answers.tools,
147149
year: new Date().getFullYear(),
148150
};
149151
}
@@ -241,7 +243,7 @@ export async function applyTemplates(
241243
/**
242244
* This copies the template files and renders them via ejs
243245
*/
244-
async function applyTemplate(
246+
export async function applyTemplate(
245247
config: TemplateConfiguration,
246248
source: string,
247249
destination: string
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
}

packages/create-react-native-library/src/utils/prompt.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,39 @@ function validate<T extends string>(
231231
? question.choices(undefined, argv)
232232
: question.choices;
233233

234-
if (choices && choices.every((choice) => choice.value !== value)) {
235-
if (choices.length > 1) {
236-
validation = `Must be one of ${choices
237-
.map((choice) => kleur.green(choice.value))
238-
.join(', ')}`;
239-
} else if (choices[0]) {
240-
validation = `Must be '${kleur.green(choices[0].value)}'`;
234+
if (choices) {
235+
let type = question.type;
236+
237+
if (typeof question.type === 'function') {
238+
type = question.type(null, argv);
239+
}
240+
241+
if (type === 'multiselect') {
242+
if (Array.isArray(value)) {
243+
const invalidChoices = value.filter((val) =>
244+
choices.every((choice) => choice.value !== val)
245+
);
246+
247+
if (invalidChoices.length > 0) {
248+
validation = `Must be an array of ${choices
249+
.map((choice) => kleur.green(choice.value))
250+
.join(', ')}`;
251+
}
252+
} else {
253+
validation = 'Must be an array';
254+
}
241255
} else {
242-
validation = false;
256+
if (choices.every((choice) => choice.value !== value)) {
257+
if (choices.length > 1) {
258+
validation = `Must be one of ${choices
259+
.map((choice) => kleur.green(choice.value))
260+
.join(', ')}`;
261+
} else if (choices[0]) {
262+
validation = `Must be '${kleur.green(choices[0].value)}'`;
263+
} else {
264+
validation = false;
265+
}
266+
}
243267
}
244268
}
245269
}

packages/create-react-native-library/templates/common/$.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ jobs:
2525
- name: Setup
2626
uses: ./.github/actions/setup
2727

28+
<% if (tools.includes('eslint')) { -%>
2829
- name: Lint files
2930
run: yarn lint
31+
<% } -%>
3032

3133
- name: Typecheck files
3234
run: yarn typecheck

0 commit comments

Comments
 (0)