diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index 3adb67665..b66a3abe4 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -116,6 +116,7 @@ jobs: --repo-url https://test.test \ --type ${{ matrix.type }} \ --languages ${{ matrix.language }} \ + --example ${{ matrix.language == 'js' && 'expo' || 'vanilla' }} \ --no-local - name: Cache dependencies of library diff --git a/packages/create-react-native-library/src/index.ts b/packages/create-react-native-library/src/index.ts index d621b8818..563324dfa 100644 --- a/packages/create-react-native-library/src/index.ts +++ b/packages/create-react-native-library/src/index.ts @@ -8,7 +8,9 @@ import ora from 'ora'; import validateNpmPackage from 'validate-npm-package-name'; import githubUsername from 'github-username'; import prompts, { type PromptObject } from './utils/prompts'; -import generateExampleApp from './utils/generateExampleApp'; +import generateExampleApp, { + type ExampleType, +} from './utils/generateExampleApp'; import { spawn } from './utils/spawn'; import { version } from '../package.json'; @@ -117,7 +119,7 @@ type Answers = { repoUrl: string; languages: ProjectLanguages; type?: ProjectType; - example?: boolean; + example?: ExampleType; reactNativeVersion?: string; local?: boolean; }; @@ -173,6 +175,24 @@ const LANGUAGE_CHOICES: { }, ]; +const EXAMPLE_CHOICES = [ + { + title: 'Test app', + value: 'test-app', + description: "app's native code is abstracted away", + }, + { + title: 'Vanilla', + value: 'vanilla', + description: "provides access to app's native code", + }, + { + title: 'Expo', + value: 'expo', + description: 'managed expo project with web support', + }, +] as const; + const NEWARCH_DESCRIPTION = 'requires new arch (experimental)'; const BACKCOMPAT_DESCRIPTION = 'supports new arch (experimental)'; @@ -260,9 +280,9 @@ const args: Record = { type: 'boolean', }, 'example': { - description: 'Whether to create an example app', - type: 'boolean', - default: true, + description: 'Type of the example app to create', + type: 'string', + choices: EXAMPLE_CHOICES.map(({ value }) => value), }, }; @@ -452,51 +472,76 @@ async function create(_argv: yargs.Arguments) { }); }, }, - ]; - - // Validate arguments passed to the CLI - for (const [key, value] of Object.entries(argv)) { - if (value == null) { - continue; - } + { + type: 'select', + name: 'example', + message: 'What type of example app do you want to create?', + choices: (_, values) => { + return EXAMPLE_CHOICES.filter((choice) => { + if (values.type) { + return values.type === 'library' + ? choice.value === 'expo' + : choice.value !== 'expo'; + } - const question = questions.find((q) => q.name === key); + return true; + }); + }, + }, + ]; - if (question == null) { - continue; - } + const validate = (answers: Answers) => { + for (const [key, value] of Object.entries(answers)) { + if (value == null) { + continue; + } - let valid = question.validate ? question.validate(String(value)) : true; + const question = questions.find((q) => q.name === key); - // We also need to guard against invalid choices - // If we don't already have a validation message to provide a better error - if (typeof valid !== 'string' && 'choices' in question) { - const choices = - typeof question.choices === 'function' - ? question.choices(undefined, argv, question) - : question.choices; + if (question == null) { + continue; + } - if (choices && !choices.some((choice) => choice.value === value)) { - valid = `Supported values are - ${choices.map((c) => - kleur.green(c.value) - )}`; + let valid = question.validate ? question.validate(String(value)) : true; + + // We also need to guard against invalid choices + // If we don't already have a validation message to provide a better error + if (typeof valid !== 'string' && 'choices' in question) { + const choices = + typeof question.choices === 'function' + ? question.choices( + undefined, + // @ts-expect-error: it complains about optional values, but it should be fine + answers, + question + ) + : question.choices; + + if (choices && !choices.some((choice) => choice.value === value)) { + valid = `Supported values are - ${choices.map((c) => + kleur.green(c.value) + )}`; + } } - } - if (valid !== true) { - let message = `Invalid value ${kleur.red( - String(value) - )} passed for ${kleur.blue(key)}`; + if (valid !== true) { + let message = `Invalid value ${kleur.red( + String(value) + )} passed for ${kleur.blue(key)}`; - if (typeof valid === 'string') { - message += `: ${valid}`; - } + if (typeof valid === 'string') { + message += `: ${valid}`; + } - console.log(message); + console.log(message); - process.exit(1); + process.exit(1); + } } - } + }; + + // Validate arguments passed to the CLI + validate(argv); const answers = { ...argv, @@ -546,6 +591,8 @@ async function create(_argv: yargs.Arguments) { )), } as Answers; + validate(answers); + const { slug, description, @@ -555,7 +602,7 @@ async function create(_argv: yargs.Arguments) { repoUrl, type = 'module-mixed', languages = type === 'library' ? 'js' : 'java-objc', - example: hasExample, + example = local ? 'none' : type === 'library' ? 'expo' : 'test-app', reactNativeVersion, } = answers; @@ -582,9 +629,6 @@ async function create(_argv: yargs.Arguments) { ? 'mixed' : 'legacy'; - const example = - hasExample && !local ? (type === 'library' ? 'expo' : 'native') : 'none'; - const project = slug.replace(/^(react-native-|@[^/]+\/)/, ''); let namespace: string | undefined; diff --git a/packages/create-react-native-library/src/utils/generateExampleApp.ts b/packages/create-react-native-library/src/utils/generateExampleApp.ts index 8cf01752e..dbb8941df 100644 --- a/packages/create-react-native-library/src/utils/generateExampleApp.ts +++ b/packages/create-react-native-library/src/utils/generateExampleApp.ts @@ -3,6 +3,8 @@ import path from 'path'; import https from 'https'; import { spawn } from './spawn'; +export type ExampleType = 'vanilla' | 'test-app' | 'expo' | 'none'; + const FILES_TO_DELETE = [ '__tests__', '.buckconfig', @@ -51,7 +53,7 @@ export default async function generateExampleApp({ arch, reactNativeVersion = 'latest', }: { - type: 'expo' | 'native'; + type: ExampleType; dest: string; slug: string; projectName: string; @@ -59,31 +61,62 @@ export default async function generateExampleApp({ reactNativeVersion?: string; }) { const directory = path.join(dest, 'example'); - const args = - type === 'native' - ? // `npx react-native init --directory example --skip-install` - [ - `react-native@${reactNativeVersion}`, - 'init', - `${projectName}Example`, - '--directory', - directory, - '--version', - reactNativeVersion, - '--skip-install', - '--npm', - ] - : // `npx create-expo-app example --no-install --template blank` - [ - 'create-expo-app@latest', - directory, - '--no-install', - '--template', - 'blank', - ]; + + // `npx --package react-native-test-app@latest init --name ${projectName}Example --destination example --version ${reactNativeVersion}` + const testAppArgs = [ + '--package', + `react-native-test-app@latest`, + 'init', + '--name', + `${projectName}Example`, + `--destination`, + directory, + ...(reactNativeVersion !== 'latest' + ? ['--version', reactNativeVersion] + : []), + '--platform', + 'ios', + '--platform', + 'android', + ]; + + // `npx react-native init --directory example --skip-install` + const vanillaArgs = [ + `react-native@${reactNativeVersion}`, + 'init', + `${projectName}Example`, + '--directory', + directory, + '--version', + reactNativeVersion, + '--skip-install', + '--npm', + ]; + + // `npx create-expo-app example --no-install --template blank` + const expoArgs = [ + 'create-expo-app@latest', + directory, + '--no-install', + '--template', + 'blank', + ]; + + let args: string[] = []; + + switch (type) { + case 'vanilla': + args = vanillaArgs; + break; + case 'test-app': + args = testAppArgs; + break; + case 'expo': + args = expoArgs; + break; + } await spawn('npx', args, { - cwd: dest, env: { ...process.env, npm_config_yes: 'true' }, }); @@ -113,7 +146,7 @@ export default async function generateExampleApp({ 'build:ios': `react-native build-ios --scheme ${projectName}Example --mode Debug --extra-params "-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO"`, }; - if (type === 'native') { + if (type !== 'expo') { Object.assign(scripts, SCRIPTS_TO_ADD); } @@ -164,7 +197,7 @@ export default async function generateExampleApp({ spaces: 2, }); - if (type === 'native') { + if (type !== 'expo') { let gradleProperties = await fs.readFile( path.join(directory, 'android', 'gradle.properties'), 'utf8' diff --git a/packages/create-react-native-library/templates/common/$package.json b/packages/create-react-native-library/templates/common/$package.json index 5cf2ca060..f0388daa4 100644 --- a/packages/create-react-native-library/templates/common/$package.json +++ b/packages/create-react-native-library/templates/common/$package.json @@ -32,7 +32,7 @@ "test": "jest", "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", -<% if (example === 'native') { -%> +<% if (example !== 'expo') { -%> "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", <% } else { -%> "clean": "del-cli lib", @@ -76,7 +76,7 @@ "react-native": "0.73.0", "react-native-builder-bob": "^<%- bob.version %>", "release-it": "^15.0.0", -<% if (example === 'native') { -%> +<% if (example !== 'expo') { -%> "turbo": "^1.10.7", <% } -%> "typescript": "^5.2.2" diff --git a/packages/create-react-native-library/templates/common/README.md b/packages/create-react-native-library/templates/common/README.md index ea2b491f0..4fe83e1b3 100644 --- a/packages/create-react-native-library/templates/common/README.md +++ b/packages/create-react-native-library/templates/common/README.md @@ -11,6 +11,7 @@ npm install <%- project.slug %> ## Usage <% if (project.view) { -%> + ```js import { <%- project.name -%>View } from "<%- project.slug -%>"; @@ -18,6 +19,7 @@ import { <%- project.name -%>View } from "<%- project.slug -%>"; <<%- project.name -%>View color="tomato" /> ``` + <% } else if (project.arch === 'new' && project.module) { -%> ```js @@ -27,7 +29,9 @@ import { multiply } from '<%- project.slug -%>'; const result = multiply(3, 7); ``` + <% } else { -%> + ```js import { multiply } from '<%- project.slug -%>'; @@ -35,6 +39,7 @@ import { multiply } from '<%- project.slug -%>'; const result = await multiply(3, 7); ``` + <% } -%> ## Contributing diff --git a/packages/create-react-native-library/templates/example-legacy/example/react-native.config.js b/packages/create-react-native-library/templates/example-legacy/example/react-native.config.js index a5166956f..c1d96d3c6 100644 --- a/packages/create-react-native-library/templates/example-legacy/example/react-native.config.js +++ b/packages/create-react-native-library/templates/example-legacy/example/react-native.config.js @@ -1,7 +1,27 @@ const path = require('path'); const pak = require('../package.json'); +<% if (example === 'test-app') { -%> +const { configureProjects } = require('react-native-test-app'); +<% } -%> module.exports = { +<% if (example === 'test-app') { -%> + project: configureProjects({ + android: { + sourceDir: 'android', + }, + ios: { + sourceDir: 'ios', + automaticPodsInstallation: true, + }, + }), +<% } else { -%> + project: { + ios: { + automaticPodsInstallation: true, + }, + }, +<% } -%> dependencies: { [pak.name]: { root: path.join(__dirname, '..'), diff --git a/packages/create-react-native-library/templates/native-common-example/example/react-native.config.js b/packages/create-react-native-library/templates/native-common-example/example/react-native.config.js index a5166956f..c1d96d3c6 100644 --- a/packages/create-react-native-library/templates/native-common-example/example/react-native.config.js +++ b/packages/create-react-native-library/templates/native-common-example/example/react-native.config.js @@ -1,7 +1,27 @@ const path = require('path'); const pak = require('../package.json'); +<% if (example === 'test-app') { -%> +const { configureProjects } = require('react-native-test-app'); +<% } -%> module.exports = { +<% if (example === 'test-app') { -%> + project: configureProjects({ + android: { + sourceDir: 'android', + }, + ios: { + sourceDir: 'ios', + automaticPodsInstallation: true, + }, + }), +<% } else { -%> + project: { + ios: { + automaticPodsInstallation: true, + }, + }, +<% } -%> dependencies: { [pak.name]: { root: path.join(__dirname, '..'), diff --git a/packages/create-react-native-library/templates/native-common-example/turbo.json b/packages/create-react-native-library/templates/native-common-example/turbo.json index 385880056..380df843a 100644 --- a/packages/create-react-native-library/templates/native-common-example/turbo.json +++ b/packages/create-react-native-library/templates/native-common-example/turbo.json @@ -1,7 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "pipeline": { -<% if (example === 'native') { -%> +<% if (example !== 'expo') { -%> "build:android": { "inputs": [ "package.json",