Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(crnl): store metadata with new projects #551

Merged
merged 16 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
133 changes: 92 additions & 41 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,41 +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]) => {
questions
.filter((question) => {
// Skip questions which are passed as parameter and pass validation
if (argv[k] != null && v.validate?.(argv[k]) !== false) {
if (
argv[question.name] != null &&
question.validate?.(argv[question.name]) !== 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 @@ -541,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 @@ -590,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 @@ -787,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 @@ -908,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