Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/build-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
--type ${{ matrix.type.name }}
--languages ${{ matrix.type.language }}
--example ${{ matrix.type.language == 'js' && 'expo' || 'vanilla' }}
--tools eslint lefthook release-it jest
)

if [[ ${{ github.event_name }} == 'schedule' ]]; then
Expand Down
12 changes: 12 additions & 0 deletions packages/create-react-native-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { determinePackageManager } from './utils/packageManager';
import { prompt } from './utils/prompt';
import { resolveNpmPackageVersion } from './utils/resolveNpmPackageVersion';
import { hideBin } from 'yargs/helpers';
import { configureTools } from './utils/configureTools';

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

if (!answers.local && answers.tools.length > 0) {
spinner.text = 'Configuring tools';

await configureTools({
tools: answers.tools,
config,
root: folder,
packageJson: rootPackageJson,
});
}

const libraryMetadata = createMetadata(answers);

rootPackageJson['create-react-native-library'] = libraryMetadata;
Expand Down
18 changes: 18 additions & 0 deletions packages/create-react-native-library/src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { version } from '../package.json';
import { SUPPORTED_REACT_NATIVE_VERSION } from './constants';
import type { Question } from './utils/prompt';
import { spawn } from './utils/spawn';
import { AVAILABLE_TOOLS } from './utils/configureTools';

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

Expand Down Expand Up @@ -145,6 +146,11 @@ export const acceptedArgs = {
type: 'string',
choices: EXAMPLE_CHOICES.map(({ value }) => value),
},
'tools': {
description: 'Tools to configure',
type: 'array',
choices: Object.keys(AVAILABLE_TOOLS),
},
'interactive': {
description: 'Whether to run in interactive mode',
type: 'boolean',
Expand All @@ -164,6 +170,7 @@ type PromptAnswers = {
languages: ProjectLanguages;
type: ProjectType;
example: ExampleApp;
tools: string[];
local: boolean;
};

Expand Down Expand Up @@ -386,6 +393,17 @@ export async function createQuestions({
});
},
},
{
type: (_, answers) => ((answers.local ?? local) ? null : 'multiselect'),
name: 'tools',
message: 'Which tools do you want to configure?',
choices: Object.entries(AVAILABLE_TOOLS).map(([key, tool]) => ({
value: key,
title: tool.name,
description: tool.description,
selected: true,
})),
},
];

return questions;
Expand Down
4 changes: 3 additions & 1 deletion packages/create-react-native-library/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type TemplateConfiguration = {
repo: string;
example: ExampleApp;
year: number;
tools: string[];
};

const BINARIES = [
Expand Down Expand Up @@ -144,6 +145,7 @@ export function generateTemplateConfiguration({
},
repo: answers.repoUrl,
example: answers.example,
tools: answers.tools,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Undefined Tools Break Configuration Templates

When creating a local library, answers.tools is undefined because the tools question is skipped (type is null when local is true). This undefined value gets assigned to config.tools and causes runtime errors in EJS templates that call tools.includes() method, since undefined doesn't have an includes method. The field should default to an empty array when undefined.

Fix in Cursor Fix in Web

year: new Date().getFullYear(),
};
}
Expand Down Expand Up @@ -241,7 +243,7 @@ export async function applyTemplates(
/**
* This copies the template files and renders them via ejs
*/
async function applyTemplate(
export async function applyTemplate(
config: TemplateConfiguration,
source: string,
destination: string
Expand Down
207 changes: 207 additions & 0 deletions packages/create-react-native-library/src/utils/configureTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import fs from 'fs-extra';
import path from 'node:path';
import { applyTemplate, type TemplateConfiguration } from '../template';
import sortObjectKeys from './sortObjectKeys';

type Tool = {
name: string;
description: string;
package: Record<string, unknown>;
condition?: (config: TemplateConfiguration) => boolean;
};

type Options = {
tools: string[];
root: string;
packageJson: Record<string, unknown>;
config: TemplateConfiguration;
};

const ESLINT = {
name: 'ESLint with Prettier',
description: 'Lint and format code',
package: {
scripts: {
lint: 'eslint "**/*.{js,ts,tsx}"',
},
prettier: {
quoteProps: 'consistent',
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: false,
},
devDependencies: {
'@eslint/compat': '^1.3.2',
'@eslint/eslintrc': '^3.3.1',
'@eslint/js': '^9.35.0',
'@react-native/eslint-config': '^0.81.1',
'eslint-config-prettier': '^10.1.8',
'eslint-plugin-prettier': '^5.5.4',
'eslint': '^9.35.0',
'prettier': '^2.8.8',
},
},
};

const LEFTHOOK = {
name: 'Lefthook with Commitlint',
description: 'Manage Git hooks and lint commit messages',
package: {
commitlint: {
extends: ['@commitlint/config-conventional'],
},
devDependencies: {
'@commitlint/config-conventional': '^19.8.1',
'commitlint': '^19.8.1',
'lefthook': '^2.0.3',
},
},
};

const RELEASE_IT = {
name: 'Release It',
description: 'Automate versioning and package publishing tasks',
package: {
'scripts': {
release: 'release-it --only-version',
},
'release-it': {
git: {
// eslint-disable-next-line no-template-curly-in-string
commitMessage: 'chore: release ${version}',
// eslint-disable-next-line no-template-curly-in-string
tagName: 'v${version}',
},
npm: {
publish: true,
},
github: {
release: true,
},
plugins: {
'@release-it/conventional-changelog': {
preset: {
name: 'angular',
},
},
},
},
'devDependencies': {
'release-it': '^19.0.4',
'@release-it/conventional-changelog': '^10.0.1',
},
},
};

const JEST = {
name: 'Jest',
description: 'Test JavaScript and TypeScript code',
package: {
scripts: {
test: 'jest',
},
jest: {
preset: 'react-native',
modulePathIgnorePatterns: [
'<rootDir>/example/node_modules',
'<rootDir>/lib/',
],
},
devDependencies: {
'@types/jest': '^29.5.14',
'jest': '^29.7.0',
},
},
};

const TURBOREPO = {
name: 'Turborepo',
description: 'Cache build outputs on CI',
package: {
devDependencies: {
turbo: '^2.5.6',
},
},
condition: (config: TemplateConfiguration) => config.example !== 'expo',
};

export const AVAILABLE_TOOLS = {
'eslint': ESLINT,
'lefthook': LEFTHOOK,
'release-it': RELEASE_IT,
'jest': JEST,
} as const satisfies Record<string, Tool>;

const REQUIRED_TOOLS = {
turbo: TURBOREPO,
} as const satisfies Record<string, Tool>;

const ALL_TOOLS = {
...AVAILABLE_TOOLS,
...REQUIRED_TOOLS,
} as const;

export async function configureTools({
tools,
config,
root,
packageJson,
}: Options) {
for (const key of [
...tools,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...(Object.keys(REQUIRED_TOOLS) as (keyof typeof REQUIRED_TOOLS)[]),
]) {
if (!(key in ALL_TOOLS)) {
throw new Error(
`Invalid tool '${key}'. Available tools are: ${Object.keys(
AVAILABLE_TOOLS
).join(', ')}.`
);
}

// @ts-expect-error: We checked the key above
const tool: Tool = ALL_TOOLS[key];

if (tool.condition && !tool.condition(config)) {
continue;
}

const files = path.resolve(__dirname, `../../templates/tools/${key}`);

if (fs.existsSync(files)) {
await applyTemplate(config, files, root);
}

for (const [key, value] of Object.entries(tool.package)) {
if (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
) {
if (typeof packageJson[key] === 'object' || packageJson[key] == null) {
packageJson[key] = {
...packageJson[key],
...value,
};

if (
key === 'dependencies' ||
key === 'devDependencies' ||
key === 'peerDependencies'
) {
// @ts-expect-error: We know they are objects here
packageJson[key] = sortObjectKeys(packageJson[key]);
}
} else {
throw new Error(
`Cannot merge '${key}' field because it is not an object (got '${String(packageJson[key])}').`
);
}
} else {
packageJson[key] = value;
}
}
}
}
40 changes: 32 additions & 8 deletions packages/create-react-native-library/src/utils/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,39 @@ function validate<T extends string>(
? question.choices(undefined, argv)
: question.choices;

if (choices && choices.every((choice) => choice.value !== value)) {
if (choices.length > 1) {
validation = `Must be one of ${choices
.map((choice) => kleur.green(choice.value))
.join(', ')}`;
} else if (choices[0]) {
validation = `Must be '${kleur.green(choices[0].value)}'`;
if (choices) {
let type = question.type;

if (typeof question.type === 'function') {
type = question.type(null, argv);
}

if (type === 'multiselect') {
if (Array.isArray(value)) {
const invalidChoices = value.filter((val) =>
choices.every((choice) => choice.value !== val)
);

if (invalidChoices.length > 0) {
validation = `Must be an array of ${choices
.map((choice) => kleur.green(choice.value))
.join(', ')}`;
}
} else {
validation = 'Must be an array';
}
} else {
validation = false;
if (choices.every((choice) => choice.value !== value)) {
if (choices.length > 1) {
validation = `Must be one of ${choices
.map((choice) => kleur.green(choice.value))
.join(', ')}`;
} else if (choices[0]) {
validation = `Must be '${kleur.green(choices[0].value)}'`;
} else {
validation = false;
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

<% if (tools.includes('eslint')) { -%>
- name: Lint files
run: yarn lint
<% } -%>

- name: Typecheck files
run: yarn typecheck
Expand Down
Loading