Skip to content

Commit

Permalink
feat(crnl): store metadata with new projects (#551)
Browse files Browse the repository at this point in the history
### Summary

We need a way to track the project information since we are going to
introduce a library upgrade helper. With this PR, when you create a new
library using `create-react-native-library`, it will automatically save;

1. Version of `create-react-native-library` used to create the project
2. The library type (native module, native view, etc.)
3. The languages used (Kotlin, objective-c, etc.)

into `create-react-native-library.json` file at the root.

### Test plan

<!-- List the steps with which we can test this change. Provide
screenshots if this changes anything visual. -->
1. Build and run `create-react-native-library`
2. Note down the options you have picked
3. Make sure the `create-react-native-library.json` file has been
generated at the root of your library
4. Check the version, library type, and languages fields and make sure
they are using correct data.

---------

Co-authored-by: Satyajit Sahoo <[email protected]>
  • Loading branch information
atlj and satya164 authored Jul 3, 2024
1 parent 48b0e9a commit cb8f969
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 49 deletions.
139 changes: 91 additions & 48 deletions packages/create-react-native-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import githubUsername from 'github-username';
import prompts, { type PromptObject } from './utils/prompts';
import generateExampleApp from './utils/generateExampleApp';
import { spawn } from './utils/spawn';
import { version } from '../package.json';

const FALLBACK_BOB_VERSION = '0.20.0';

Expand Down Expand Up @@ -107,6 +108,7 @@ type ProjectType =
| 'library';

type Answers = {
name: string;
slug: string;
description: string;
authorName: string;
Expand All @@ -117,6 +119,7 @@ type Answers = {
type?: ProjectType;
example?: boolean;
reactNativeVersion?: string;
local?: boolean;
};

const LANGUAGE_CHOICES: {
Expand Down Expand Up @@ -265,7 +268,10 @@ const args: Record<ArgName, yargs.Options> = {

// FIXME: fix the type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function create(argv: yargs.Arguments<any>) {
async function create(_argv: yargs.Arguments<any>) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _, $0, ...argv } = _argv;

let local = false;

if (typeof argv.local === 'boolean') {
Expand Down Expand Up @@ -355,13 +361,11 @@ async function create(argv: yargs.Arguments<any>) {

const basename = path.basename(folder);

const questions: Record<
ArgName,
Omit<PromptObject<keyof Answers>, 'validate'> & {
validate?: (value: string) => boolean | string;
}
> = {
'slug': {
const questions: (Omit<PromptObject<keyof Answers>, 'validate' | 'name'> & {
validate?: (value: string) => boolean | string;
name: string;
})[] = [
{
type: 'text',
name: 'slug',
message: 'What is the name of the npm package?',
Expand All @@ -374,28 +378,28 @@ async function create(argv: yargs.Arguments<any>) {
validateNpmPackage(input).validForNewPackages ||
'Must be a valid npm package name',
},
'description': {
{
type: 'text',
name: 'description',
message: 'What is the description for the package?',
validate: (input) => Boolean(input) || 'Cannot be empty',
},
'author-name': {
{
type: local ? null : 'text',
name: 'authorName',
message: 'What is the name of package author?',
initial: name,
validate: (input) => Boolean(input) || 'Cannot be empty',
},
'author-email': {
{
type: local ? null : 'text',
name: 'authorEmail',
message: 'What is the email address for the package author?',
initial: email,
validate: (input) =>
/^\S+@\S+$/.test(input) || 'Must be a valid email address',
},
'author-url': {
{
type: local ? null : 'text',
name: 'authorUrl',
message: 'What is the URL for the package author?',
Expand All @@ -413,7 +417,7 @@ async function create(argv: yargs.Arguments<any>) {
},
validate: (input) => /^https?:\/\//.test(input) || 'Must be a valid URL',
},
'repo-url': {
{
type: local ? null : 'text',
name: 'repoUrl',
message: 'What is the URL for the repository?',
Expand All @@ -428,13 +432,13 @@ async function create(argv: yargs.Arguments<any>) {
},
validate: (input) => /^https?:\/\//.test(input) || 'Must be a valid URL',
},
'type': {
{
type: 'select',
name: 'type',
message: 'What type of library do you want to develop?',
choices: TYPE_CHOICES,
},
'languages': {
{
type: 'select',
name: 'languages',
message: 'Which languages do you want to use?',
Expand All @@ -448,15 +452,15 @@ async function create(argv: yargs.Arguments<any>) {
});
},
},
};
];

// Validate arguments passed to the CLI
for (const [key, value] of Object.entries(argv)) {
if (value == null) {
continue;
}

const question = questions[key as ArgName];
const question = questions.find((q) => q.name === key);

if (question == null) {
continue;
Expand Down Expand Up @@ -494,49 +498,37 @@ async function create(argv: yargs.Arguments<any>) {
}
}

const {
slug,
description,
authorName,
authorEmail,
authorUrl,
repoUrl,
type = 'module-mixed',
languages = type === 'library' ? 'js' : 'java-objc',
example: hasExample,
reactNativeVersion,
} = {
const answers = {
...argv,
local,
...(await prompts(
Object.entries(questions)
.filter(([k, v]) => {
// Skip 'with-recommended-options' question if type of language is passed
questions
.filter((question) => {
// Skip questions which are passed as parameter and pass validation
if (
k === 'with-recommended-options' &&
(argv.languages || argv.type)
argv[question.name] != null &&
question.validate?.(argv[question.name]) !== false
) {
return false;
}

// Skip questions which are passed as parameter and pass validation
if (argv[k] != null && v.validate?.(argv[k]) !== false) {
return false;
}

// Skip questions with a single choice
if (Array.isArray(v.choices) && v.choices.length === 1) {
if (
Array.isArray(question.choices) &&
question.choices.length === 1
) {
return false;
}

return true;
})
.map(([, v]) => {
const { type, choices } = v;
.map((question) => {
const { type, choices } = question;

// Skip dynamic questions with a single choice
if (type === 'select' && typeof choices === 'function') {
return {
...v,
...question,
type: (prev, values, prompt) => {
const result = choices(prev, { ...argv, ...values }, prompt);

Expand All @@ -549,24 +541,37 @@ async function create(argv: yargs.Arguments<any>) {
};
}

return v;
return question;
})
)),
} as Answers;

const {
slug,
description,
authorName,
authorEmail,
authorUrl,
repoUrl,
type = 'module-mixed',
languages = type === 'library' ? 'js' : 'java-objc',
example: hasExample,
reactNativeVersion,
} = answers;

// Get latest version of Bob from NPM
let version: string;
let bobVersion: string;

try {
version = await Promise.race([
bobVersion = await Promise.race([
new Promise<string>((resolve) => {
setTimeout(() => resolve(FALLBACK_BOB_VERSION), 1000);
}),
spawn('npm', ['view', 'react-native-builder-bob', 'dist-tags.latest']),
]);
} catch (e) {
// Fallback to a known version if we couldn't fetch
version = FALLBACK_BOB_VERSION;
bobVersion = FALLBACK_BOB_VERSION;
}

const moduleType = type.startsWith('view-') ? 'view' : 'module';
Expand Down Expand Up @@ -598,7 +603,7 @@ async function create(argv: yargs.Arguments<any>) {

const options = {
bob: {
version: version || FALLBACK_BOB_VERSION,
version: bobVersion || FALLBACK_BOB_VERSION,
},
project: {
slug,
Expand Down Expand Up @@ -795,6 +800,40 @@ async function create(argv: yargs.Arguments<any>) {
}
}

// Some of the passed args can already be derived from the generated package.json file.
const ignoredAnswers: (keyof Answers)[] = [
'name',
'slug',
'description',
'authorName',
'authorEmail',
'authorUrl',
'repoUrl',
'example',
'reactNativeVersion',
'local',
];

type AnswerEntries<T extends keyof Answers = keyof Answers> = [
T,
Answers[T],
][];

const libraryMetadata = Object.fromEntries(
(Object.entries(answers) as AnswerEntries).filter(
([answer]) => !ignoredAnswers.includes(answer)
)
);
libraryMetadata.version = version;

const libraryPackageJson = await fs.readJson(
path.join(folder, 'package.json')
);
libraryPackageJson['create-react-native-library'] = libraryMetadata;
await fs.writeJson(path.join(folder, 'package.json'), libraryPackageJson, {
spaces: 2,
});

spinner.succeed(
`Project created successfully at ${kleur.yellow(
path.relative(process.cwd(), folder)
Expand Down Expand Up @@ -916,4 +955,8 @@ yargs

process.exit(1);
})
.parserConfiguration({
// don't pass kebab-case args to handler.
'strip-dashed': true,
})
.strict().argv;
1 change: 0 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"react-native-builder-bob": ["./packages/react-native-builder-bob/src"]
},
"outDir": "./typescript",
"composite": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"esModuleInterop": true,
Expand Down

0 comments on commit cb8f969

Please sign in to comment.