From 1f9f2390e470592ba8c75d3339a18e6106fd3a19 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Fri, 25 Aug 2023 14:33:55 +0200 Subject: [PATCH] feat: add ability to generate a local module (experimental) --- .github/workflows/build-templates.yml | 3 +- .../create-react-native-library/src/index.ts | 247 ++++++++++++++---- .../src/utils/generateExampleApp.ts | 7 +- .../templates/common-local/$package.json | 16 ++ .../example/react-native.config.js | 0 .../scripts/pod-install.cjs | 0 .../turbo.json | 0 7 files changed, 216 insertions(+), 57 deletions(-) create mode 100644 packages/create-react-native-library/templates/common-local/$package.json rename packages/create-react-native-library/templates/{native-common => native-common-example}/example/react-native.config.js (100%) rename packages/create-react-native-library/templates/{native-common => native-common-example}/scripts/pod-install.cjs (100%) rename packages/create-react-native-library/templates/{native-common => native-common-example}/turbo.json (100%) diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index 92c93d6d7..7f5a5eb78 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -112,7 +112,8 @@ jobs: --author-url https://test.test \ --repo-url https://test.test \ --type ${{ matrix.type }} \ - --languages ${{ matrix.language }} + --languages ${{ matrix.language }} \ + --no-local - name: Cache dependencies of library id: library-yarn-cache diff --git a/packages/create-react-native-library/src/index.ts b/packages/create-react-native-library/src/index.ts index 16dc91dfa..9f7aeb9e3 100644 --- a/packages/create-react-native-library/src/index.ts +++ b/packages/create-react-native-library/src/index.ts @@ -19,6 +19,7 @@ const BINARIES = [ ]; const COMMON_FILES = path.resolve(__dirname, '../templates/common'); +const COMMON_LOCAL_FILES = path.resolve(__dirname, '../templates/common-local'); const JS_FILES = path.resolve(__dirname, '../templates/js-library'); const EXPO_FILES = path.resolve(__dirname, '../templates/expo-library'); const CPP_FILES = path.resolve(__dirname, '../templates/cpp-library'); @@ -27,6 +28,10 @@ const NATIVE_COMMON_FILES = path.resolve( __dirname, '../templates/native-common' ); +const NATIVE_COMMON_EXAMPLE_FILES = path.resolve( + __dirname, + '../templates/native-common-example' +); const NATIVE_FILES = { module_legacy: path.resolve(__dirname, '../templates/native-library-legacy'), @@ -82,6 +87,8 @@ type ArgName = | 'repo-url' | 'languages' | 'type' + | 'local' + | 'example' | 'react-native-version'; type ProjectLanguages = @@ -110,6 +117,7 @@ type Answers = { repoUrl: string; languages: ProjectLanguages; type?: ProjectType; + example?: boolean; reactNativeVersion?: string; }; @@ -246,10 +254,60 @@ const args: Record = { description: 'Version of React Native to use, uses latest if not specified', type: 'string', }, + 'local': { + description: 'Whether to create a local library', + type: 'boolean', + }, + 'example': { + description: 'Whether to create a an example app', + type: 'boolean', + }, }; async function create(argv: yargs.Arguments) { - const folder = path.join(process.cwd(), argv.name); + let local = false; + let folder = path.join(process.cwd(), argv.name); + + if (typeof argv.local === 'boolean') { + local = argv.local; + } else { + let hasPackageJson = await fs.pathExists( + path.join(process.cwd(), 'package.json') + ); + + if (hasPackageJson) { + // If we're under a project with package.json, ask the user if they want to create a local library + const answers = await prompts([ + { + type: 'confirm', + name: 'local', + message: `Looks like you're under a project folder. Do you want to create a local library?`, + initial: true, + }, + { + type: (previous: boolean) => { + if (previous) { + return 'text'; + } + + return null; + }, + name: 'folder', + message: `Where to create the local library?`, + initial: argv.name.includes('/') + ? argv.name + : `packages/${argv.name}`, + validate: (input) => Boolean(input) || 'Cannot be empty', + }, + ]); + + local = answers.local; + + if (local) { + folder = path.join(process.cwd(), answers.folder); + } + } + } if (await fs.pathExists(folder)) { console.log( @@ -315,14 +373,14 @@ async function create(argv: yargs.Arguments) { validate: (input) => Boolean(input) || 'Cannot be empty', }, 'author-name': { - type: 'text', + 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: 'text', + type: local ? null : 'text', name: 'authorEmail', message: 'What is the email address for the package author?', initial: email, @@ -330,7 +388,7 @@ async function create(argv: yargs.Arguments) { /^\S+@\S+$/.test(input) || 'Must be a valid email address', }, 'author-url': { - type: 'text', + type: local ? null : 'text', name: 'authorUrl', message: 'What is the URL for the package author?', // @ts-ignore: this is supported, but types are wrong @@ -348,7 +406,7 @@ async function create(argv: yargs.Arguments) { validate: (input) => /^https?:\/\//.test(input) || 'Must be a valid URL', }, 'repo-url': { - type: 'text', + type: local ? null : 'text', name: 'repoUrl', message: 'What is the URL for the repository?', // @ts-ignore: this is supported, but types are wrong @@ -438,6 +496,7 @@ async function create(argv: yargs.Arguments) { repoUrl, type = 'module-mixed', languages = type === 'library' ? 'js' : 'java-objc', + example = !local, reactNativeVersion, } = { ...argv, @@ -503,7 +562,7 @@ async function create(argv: yargs.Arguments) { ? 'mixed' : 'legacy'; - const example = type === 'library' ? 'expo' : 'native'; + const exampleType = type === 'library' ? 'expo' : 'native'; const project = slug.replace(/^(react-native-|@[^/]+\/)/, ''); let namespace: string | undefined; @@ -553,7 +612,7 @@ async function create(argv: yargs.Arguments) { url: authorUrl, }, repo: repoUrl, - example, + example: exampleType, year: new Date().getFullYear(), }; @@ -589,7 +648,7 @@ async function create(argv: yargs.Arguments) { await fs.mkdirp(folder); if (reactNativeVersion != null) { - if (example === 'expo') { + if (exampleType === 'expo') { console.warn( `${kleur.yellow('⚠')} Ignoring --react-native-version for Expo example` ); @@ -602,32 +661,46 @@ async function create(argv: yargs.Arguments) { } } - const spinner = ora('Generating example').start(); + const spinner = ora().start(); - await generateExampleApp({ - type: example, - dest: folder, - slug: options.project.slug, - projectName: options.project.name, - arch, - reactNativeVersion, - }); + if (example) { + spinner.text = 'Generating example app'; + + await generateExampleApp({ + type: exampleType, + dest: folder, + slug: options.project.slug, + projectName: options.project.name, + arch, + reactNativeVersion, + }); + } spinner.text = 'Copying files'; - await copyDir(COMMON_FILES, folder); + if (local) { + await copyDir(COMMON_LOCAL_FILES, folder); + } else { + await copyDir(COMMON_FILES, folder); + } if (languages === 'js') { await copyDir(JS_FILES, folder); await copyDir(EXPO_FILES, folder); } else { - await copyDir( - path.join(EXAMPLE_FILES, 'example'), - path.join(folder, 'example') - ); + if (example) { + await copyDir( + path.join(EXAMPLE_FILES, 'example'), + path.join(folder, 'example') + ); + } await copyDir(NATIVE_COMMON_FILES, folder); + if (example) { + await copyDir(NATIVE_COMMON_EXAMPLE_FILES, folder); + } + if (moduleType === 'module') { await copyDir(NATIVE_FILES[`${moduleType}_${arch}`], folder); } else { @@ -664,44 +737,113 @@ async function create(argv: yargs.Arguments) { } } - // Set `react` and `react-native` versions of root `package.json` from example `package.json` - const examplePackageJson = fs.readJSONSync( - path.join(folder, 'example', 'package.json') - ); - const rootPackageJson = fs.readJSONSync(path.join(folder, 'package.json')); - rootPackageJson.devDependencies.react = examplePackageJson.dependencies.react; - rootPackageJson.devDependencies['react-native'] = - examplePackageJson.dependencies['react-native']; + if (example) { + // Set `react` and `react-native` versions of root `package.json` from example `package.json` + const examplePackageJson = await fs.readJSON( + path.join(folder, 'example', 'package.json') + ); + const rootPackageJson = await fs.readJSON( + path.join(folder, 'package.json') + ); - fs.writeJSONSync(path.join(folder, 'package.json'), rootPackageJson, { - spaces: 2, - }); + rootPackageJson.devDependencies.react = + examplePackageJson.dependencies.react; + rootPackageJson.devDependencies['react-native'] = + examplePackageJson.dependencies['react-native']; - try { - await spawn('git', ['init'], { cwd: folder }); - await spawn('git', ['branch', '-M', 'main'], { cwd: folder }); - await spawn('git', ['add', '.'], { cwd: folder }); - await spawn('git', ['commit', '-m', 'chore: initial commit'], { - cwd: folder, + await fs.writeJSON(path.join(folder, 'package.json'), rootPackageJson, { + spaces: 2, }); - } catch (e) { - // Ignore error + } + + if (!local) { + try { + await spawn('git', ['init'], { cwd: folder }); + await spawn('git', ['branch', '-M', 'main'], { cwd: folder }); + await spawn('git', ['add', '.'], { cwd: folder }); + await spawn('git', ['commit', '-m', 'chore: initial commit'], { + cwd: folder, + }); + } catch (e) { + // Ignore error + } } spinner.succeed( - `Project created successfully at ${kleur.yellow(argv.name)}!\n` + `Project created successfully at ${kleur.yellow( + path.relative(process.cwd(), folder) + )}!\n` ); - const platforms = { - ios: { name: 'iOS', color: 'cyan' }, - android: { name: 'Android', color: 'green' }, - ...(example === 'expo' - ? ({ web: { name: 'Web', color: 'blue' } } as const) - : null), - } as const; + if (local) { + let linked; + + const packageManager = (await fs.pathExists( + path.join(process.cwd(), 'yarn.lock') + )) + ? 'yarn' + : 'npm'; + + const packageJsonPath = path.join(process.cwd(), 'package.json'); + + if (await fs.pathExists(packageJsonPath)) { + const packageJson = await fs.readJSON(packageJsonPath); + const isReactNativeProject = Boolean( + packageJson.dependencies?.['react-native'] + ); + + if (isReactNativeProject) { + packageJson.dependencies = packageJson.dependencies || {}; + packageJson.dependencies[slug] = + packageManager === 'yarn' + ? `link:./${path.relative(process.cwd(), folder)}` + : `file:./${path.relative(process.cwd(), folder)}`; + + await fs.writeJSON(packageJsonPath, packageJson, { + spaces: 2, + }); + + linked = true; + } + } + + console.log( + dedent(` + ${kleur.magenta( + `${kleur.bold('Get started')} with the project` + )}${kleur.gray(':')} + + ${ + (linked + ? `- Run ${kleur.blue( + `${packageManager} install` + )} to link the library\n` + : `- Link the library at ${kleur.blue( + path.relative(process.cwd(), folder) + )} based on your project setup'\n`) + + `- Run ${kleur.blue( + 'cd ios; pod install; cd -' + )} to install dependencies with CocoaPods\n` + + `- Run ${kleur.blue('npx react-native run-android')} or ${kleur.blue( + 'npx react-native run-ios' + )} to build and run the app\n` + + `- Import from ${kleur.blue(slug)} and use it in your app.` + } - console.log( - dedent(` + ${kleur.yellow(`Good luck!`)} + `) + ); + } else { + const platforms = { + ios: { name: 'iOS', color: 'cyan' }, + android: { name: 'Android', color: 'green' }, + ...(exampleType === 'expo' + ? ({ web: { name: 'Web', color: 'blue' } } as const) + : null), + } as const; + + console.log( + dedent(` ${kleur.magenta( `${kleur.bold('Get started')} with the project` )}${kleur.gray(':')} @@ -722,7 +864,8 @@ async function create(argv: yargs.Arguments) { `See ${kleur.bold('CONTRIBUTING.md')} for more details. Good luck!` )} `) - ); + ); + } } // eslint-disable-next-line babel/no-unused-expressions yargs diff --git a/packages/create-react-native-library/src/utils/generateExampleApp.ts b/packages/create-react-native-library/src/utils/generateExampleApp.ts index d33e411ce..91588b2fd 100644 --- a/packages/create-react-native-library/src/utils/generateExampleApp.ts +++ b/packages/create-react-native-library/src/utils/generateExampleApp.ts @@ -162,10 +162,9 @@ export default async function generateExampleApp({ scripts.web = 'expo start --web'; } - await fs.writeFile( - path.join(directory, 'package.json'), - JSON.stringify(pkg, null, 2) - ); + await fs.writeJSON(path.join(directory, 'package.json'), pkg, { + spaces: 2, + }); // If the library is on new architecture, enable new arch for iOS and Android if (arch === 'new') { diff --git a/packages/create-react-native-library/templates/common-local/$package.json b/packages/create-react-native-library/templates/common-local/$package.json new file mode 100644 index 000000000..e3b230994 --- /dev/null +++ b/packages/create-react-native-library/templates/common-local/$package.json @@ -0,0 +1,16 @@ +{ + "name": "<%- project.slug -%>", + "version": "0.0.0", + "description": "<%- project.description %>", + "main": "src/index", +<% if (project.arch !== 'legacy') { -%> + "codegenConfig": { + "name": "RN<%- project.name -%><%- project.view ? 'View': '' -%>Spec", + "type": <%- project.view ? '"components"': '"modules"' %>, + "jsSrcsDir": "src" + }, +<% } -%> + "author": "<%- author.name -%> <<%- author.email -%>> (<%- author.url -%>)", + "license": "UNLICENSED", + "homepage": "<%- repo -%>#readme" +} diff --git a/packages/create-react-native-library/templates/native-common/example/react-native.config.js b/packages/create-react-native-library/templates/native-common-example/example/react-native.config.js similarity index 100% rename from packages/create-react-native-library/templates/native-common/example/react-native.config.js rename to packages/create-react-native-library/templates/native-common-example/example/react-native.config.js diff --git a/packages/create-react-native-library/templates/native-common/scripts/pod-install.cjs b/packages/create-react-native-library/templates/native-common-example/scripts/pod-install.cjs similarity index 100% rename from packages/create-react-native-library/templates/native-common/scripts/pod-install.cjs rename to packages/create-react-native-library/templates/native-common-example/scripts/pod-install.cjs diff --git a/packages/create-react-native-library/templates/native-common/turbo.json b/packages/create-react-native-library/templates/native-common-example/turbo.json similarity index 100% rename from packages/create-react-native-library/templates/native-common/turbo.json rename to packages/create-react-native-library/templates/native-common-example/turbo.json