Skip to content

Commit 1f9f239

Browse files
committed
feat: add ability to generate a local module (experimental)
1 parent 93f2907 commit 1f9f239

File tree

7 files changed

+216
-57
lines changed

7 files changed

+216
-57
lines changed

.github/workflows/build-templates.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ jobs:
112112
--author-url https://test.test \
113113
--repo-url https://test.test \
114114
--type ${{ matrix.type }} \
115-
--languages ${{ matrix.language }}
115+
--languages ${{ matrix.language }} \
116+
--no-local
116117
117118
- name: Cache dependencies of library
118119
id: library-yarn-cache

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

Lines changed: 195 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const BINARIES = [
1919
];
2020

2121
const COMMON_FILES = path.resolve(__dirname, '../templates/common');
22+
const COMMON_LOCAL_FILES = path.resolve(__dirname, '../templates/common-local');
2223
const JS_FILES = path.resolve(__dirname, '../templates/js-library');
2324
const EXPO_FILES = path.resolve(__dirname, '../templates/expo-library');
2425
const CPP_FILES = path.resolve(__dirname, '../templates/cpp-library');
@@ -27,6 +28,10 @@ const NATIVE_COMMON_FILES = path.resolve(
2728
__dirname,
2829
'../templates/native-common'
2930
);
31+
const NATIVE_COMMON_EXAMPLE_FILES = path.resolve(
32+
__dirname,
33+
'../templates/native-common-example'
34+
);
3035

3136
const NATIVE_FILES = {
3237
module_legacy: path.resolve(__dirname, '../templates/native-library-legacy'),
@@ -82,6 +87,8 @@ type ArgName =
8287
| 'repo-url'
8388
| 'languages'
8489
| 'type'
90+
| 'local'
91+
| 'example'
8592
| 'react-native-version';
8693

8794
type ProjectLanguages =
@@ -110,6 +117,7 @@ type Answers = {
110117
repoUrl: string;
111118
languages: ProjectLanguages;
112119
type?: ProjectType;
120+
example?: boolean;
113121
reactNativeVersion?: string;
114122
};
115123

@@ -246,10 +254,60 @@ const args: Record<ArgName, yargs.Options> = {
246254
description: 'Version of React Native to use, uses latest if not specified',
247255
type: 'string',
248256
},
257+
'local': {
258+
description: 'Whether to create a local library',
259+
type: 'boolean',
260+
},
261+
'example': {
262+
description: 'Whether to create a an example app',
263+
type: 'boolean',
264+
},
249265
};
250266

251267
async function create(argv: yargs.Arguments<any>) {
252-
const folder = path.join(process.cwd(), argv.name);
268+
let local = false;
269+
let folder = path.join(process.cwd(), argv.name);
270+
271+
if (typeof argv.local === 'boolean') {
272+
local = argv.local;
273+
} else {
274+
let hasPackageJson = await fs.pathExists(
275+
path.join(process.cwd(), 'package.json')
276+
);
277+
278+
if (hasPackageJson) {
279+
// If we're under a project with package.json, ask the user if they want to create a local library
280+
const answers = await prompts([
281+
{
282+
type: 'confirm',
283+
name: 'local',
284+
message: `Looks like you're under a project folder. Do you want to create a local library?`,
285+
initial: true,
286+
},
287+
{
288+
type: (previous: boolean) => {
289+
if (previous) {
290+
return 'text';
291+
}
292+
293+
return null;
294+
},
295+
name: 'folder',
296+
message: `Where to create the local library?`,
297+
initial: argv.name.includes('/')
298+
? argv.name
299+
: `packages/${argv.name}`,
300+
validate: (input) => Boolean(input) || 'Cannot be empty',
301+
},
302+
]);
303+
304+
local = answers.local;
305+
306+
if (local) {
307+
folder = path.join(process.cwd(), answers.folder);
308+
}
309+
}
310+
}
253311

254312
if (await fs.pathExists(folder)) {
255313
console.log(
@@ -315,22 +373,22 @@ async function create(argv: yargs.Arguments<any>) {
315373
validate: (input) => Boolean(input) || 'Cannot be empty',
316374
},
317375
'author-name': {
318-
type: 'text',
376+
type: local ? null : 'text',
319377
name: 'authorName',
320378
message: 'What is the name of package author?',
321379
initial: name,
322380
validate: (input) => Boolean(input) || 'Cannot be empty',
323381
},
324382
'author-email': {
325-
type: 'text',
383+
type: local ? null : 'text',
326384
name: 'authorEmail',
327385
message: 'What is the email address for the package author?',
328386
initial: email,
329387
validate: (input) =>
330388
/^\S+@\S+$/.test(input) || 'Must be a valid email address',
331389
},
332390
'author-url': {
333-
type: 'text',
391+
type: local ? null : 'text',
334392
name: 'authorUrl',
335393
message: 'What is the URL for the package author?',
336394
// @ts-ignore: this is supported, but types are wrong
@@ -348,7 +406,7 @@ async function create(argv: yargs.Arguments<any>) {
348406
validate: (input) => /^https?:\/\//.test(input) || 'Must be a valid URL',
349407
},
350408
'repo-url': {
351-
type: 'text',
409+
type: local ? null : 'text',
352410
name: 'repoUrl',
353411
message: 'What is the URL for the repository?',
354412
// @ts-ignore: this is supported, but types are wrong
@@ -438,6 +496,7 @@ async function create(argv: yargs.Arguments<any>) {
438496
repoUrl,
439497
type = 'module-mixed',
440498
languages = type === 'library' ? 'js' : 'java-objc',
499+
example = !local,
441500
reactNativeVersion,
442501
} = {
443502
...argv,
@@ -503,7 +562,7 @@ async function create(argv: yargs.Arguments<any>) {
503562
? 'mixed'
504563
: 'legacy';
505564

506-
const example = type === 'library' ? 'expo' : 'native';
565+
const exampleType = type === 'library' ? 'expo' : 'native';
507566
const project = slug.replace(/^(react-native-|@[^/]+\/)/, '');
508567

509568
let namespace: string | undefined;
@@ -553,7 +612,7 @@ async function create(argv: yargs.Arguments<any>) {
553612
url: authorUrl,
554613
},
555614
repo: repoUrl,
556-
example,
615+
example: exampleType,
557616
year: new Date().getFullYear(),
558617
};
559618

@@ -589,7 +648,7 @@ async function create(argv: yargs.Arguments<any>) {
589648
await fs.mkdirp(folder);
590649

591650
if (reactNativeVersion != null) {
592-
if (example === 'expo') {
651+
if (exampleType === 'expo') {
593652
console.warn(
594653
`${kleur.yellow('⚠')} Ignoring --react-native-version for Expo example`
595654
);
@@ -602,32 +661,46 @@ async function create(argv: yargs.Arguments<any>) {
602661
}
603662
}
604663

605-
const spinner = ora('Generating example').start();
664+
const spinner = ora().start();
606665

607-
await generateExampleApp({
608-
type: example,
609-
dest: folder,
610-
slug: options.project.slug,
611-
projectName: options.project.name,
612-
arch,
613-
reactNativeVersion,
614-
});
666+
if (example) {
667+
spinner.text = 'Generating example app';
668+
669+
await generateExampleApp({
670+
type: exampleType,
671+
dest: folder,
672+
slug: options.project.slug,
673+
projectName: options.project.name,
674+
arch,
675+
reactNativeVersion,
676+
});
677+
}
615678

616679
spinner.text = 'Copying files';
617680

618-
await copyDir(COMMON_FILES, folder);
681+
if (local) {
682+
await copyDir(COMMON_LOCAL_FILES, folder);
683+
} else {
684+
await copyDir(COMMON_FILES, folder);
685+
}
619686

620687
if (languages === 'js') {
621688
await copyDir(JS_FILES, folder);
622689
await copyDir(EXPO_FILES, folder);
623690
} else {
624-
await copyDir(
625-
path.join(EXAMPLE_FILES, 'example'),
626-
path.join(folder, 'example')
627-
);
691+
if (example) {
692+
await copyDir(
693+
path.join(EXAMPLE_FILES, 'example'),
694+
path.join(folder, 'example')
695+
);
696+
}
628697

629698
await copyDir(NATIVE_COMMON_FILES, folder);
630699

700+
if (example) {
701+
await copyDir(NATIVE_COMMON_EXAMPLE_FILES, folder);
702+
}
703+
631704
if (moduleType === 'module') {
632705
await copyDir(NATIVE_FILES[`${moduleType}_${arch}`], folder);
633706
} else {
@@ -664,44 +737,113 @@ async function create(argv: yargs.Arguments<any>) {
664737
}
665738
}
666739

667-
// Set `react` and `react-native` versions of root `package.json` from example `package.json`
668-
const examplePackageJson = fs.readJSONSync(
669-
path.join(folder, 'example', 'package.json')
670-
);
671-
const rootPackageJson = fs.readJSONSync(path.join(folder, 'package.json'));
672-
rootPackageJson.devDependencies.react = examplePackageJson.dependencies.react;
673-
rootPackageJson.devDependencies['react-native'] =
674-
examplePackageJson.dependencies['react-native'];
740+
if (example) {
741+
// Set `react` and `react-native` versions of root `package.json` from example `package.json`
742+
const examplePackageJson = await fs.readJSON(
743+
path.join(folder, 'example', 'package.json')
744+
);
745+
const rootPackageJson = await fs.readJSON(
746+
path.join(folder, 'package.json')
747+
);
675748

676-
fs.writeJSONSync(path.join(folder, 'package.json'), rootPackageJson, {
677-
spaces: 2,
678-
});
749+
rootPackageJson.devDependencies.react =
750+
examplePackageJson.dependencies.react;
751+
rootPackageJson.devDependencies['react-native'] =
752+
examplePackageJson.dependencies['react-native'];
679753

680-
try {
681-
await spawn('git', ['init'], { cwd: folder });
682-
await spawn('git', ['branch', '-M', 'main'], { cwd: folder });
683-
await spawn('git', ['add', '.'], { cwd: folder });
684-
await spawn('git', ['commit', '-m', 'chore: initial commit'], {
685-
cwd: folder,
754+
await fs.writeJSON(path.join(folder, 'package.json'), rootPackageJson, {
755+
spaces: 2,
686756
});
687-
} catch (e) {
688-
// Ignore error
757+
}
758+
759+
if (!local) {
760+
try {
761+
await spawn('git', ['init'], { cwd: folder });
762+
await spawn('git', ['branch', '-M', 'main'], { cwd: folder });
763+
await spawn('git', ['add', '.'], { cwd: folder });
764+
await spawn('git', ['commit', '-m', 'chore: initial commit'], {
765+
cwd: folder,
766+
});
767+
} catch (e) {
768+
// Ignore error
769+
}
689770
}
690771

691772
spinner.succeed(
692-
`Project created successfully at ${kleur.yellow(argv.name)}!\n`
773+
`Project created successfully at ${kleur.yellow(
774+
path.relative(process.cwd(), folder)
775+
)}!\n`
693776
);
694777

695-
const platforms = {
696-
ios: { name: 'iOS', color: 'cyan' },
697-
android: { name: 'Android', color: 'green' },
698-
...(example === 'expo'
699-
? ({ web: { name: 'Web', color: 'blue' } } as const)
700-
: null),
701-
} as const;
778+
if (local) {
779+
let linked;
780+
781+
const packageManager = (await fs.pathExists(
782+
path.join(process.cwd(), 'yarn.lock')
783+
))
784+
? 'yarn'
785+
: 'npm';
786+
787+
const packageJsonPath = path.join(process.cwd(), 'package.json');
788+
789+
if (await fs.pathExists(packageJsonPath)) {
790+
const packageJson = await fs.readJSON(packageJsonPath);
791+
const isReactNativeProject = Boolean(
792+
packageJson.dependencies?.['react-native']
793+
);
794+
795+
if (isReactNativeProject) {
796+
packageJson.dependencies = packageJson.dependencies || {};
797+
packageJson.dependencies[slug] =
798+
packageManager === 'yarn'
799+
? `link:./${path.relative(process.cwd(), folder)}`
800+
: `file:./${path.relative(process.cwd(), folder)}`;
801+
802+
await fs.writeJSON(packageJsonPath, packageJson, {
803+
spaces: 2,
804+
});
805+
806+
linked = true;
807+
}
808+
}
809+
810+
console.log(
811+
dedent(`
812+
${kleur.magenta(
813+
`${kleur.bold('Get started')} with the project`
814+
)}${kleur.gray(':')}
815+
816+
${
817+
(linked
818+
? `- Run ${kleur.blue(
819+
`${packageManager} install`
820+
)} to link the library\n`
821+
: `- Link the library at ${kleur.blue(
822+
path.relative(process.cwd(), folder)
823+
)} based on your project setup'\n`) +
824+
`- Run ${kleur.blue(
825+
'cd ios; pod install; cd -'
826+
)} to install dependencies with CocoaPods\n` +
827+
`- Run ${kleur.blue('npx react-native run-android')} or ${kleur.blue(
828+
'npx react-native run-ios'
829+
)} to build and run the app\n` +
830+
`- Import from ${kleur.blue(slug)} and use it in your app.`
831+
}
702832
703-
console.log(
704-
dedent(`
833+
${kleur.yellow(`Good luck!`)}
834+
`)
835+
);
836+
} else {
837+
const platforms = {
838+
ios: { name: 'iOS', color: 'cyan' },
839+
android: { name: 'Android', color: 'green' },
840+
...(exampleType === 'expo'
841+
? ({ web: { name: 'Web', color: 'blue' } } as const)
842+
: null),
843+
} as const;
844+
845+
console.log(
846+
dedent(`
705847
${kleur.magenta(
706848
`${kleur.bold('Get started')} with the project`
707849
)}${kleur.gray(':')}
@@ -722,7 +864,8 @@ async function create(argv: yargs.Arguments<any>) {
722864
`See ${kleur.bold('CONTRIBUTING.md')} for more details. Good luck!`
723865
)}
724866
`)
725-
);
867+
);
868+
}
726869
}
727870
// eslint-disable-next-line babel/no-unused-expressions
728871
yargs

0 commit comments

Comments
 (0)