diff --git a/docs/pages/create.md b/docs/pages/create.md index 4fbffd415..0c070d0cc 100644 --- a/docs/pages/create.md +++ b/docs/pages/create.md @@ -11,8 +11,8 @@ If you want to create your own React Native library, scaffolding the project can - [TypeScript](https://www.typescriptlang.org/) to ensure type-safe code and better DX - Support for [Turbo Modules](https://reactnative.dev/docs/turbo-native-modules-introduction) & [Fabric](https://reactnative.dev/docs/fabric-native-components-introduction) - Support for [Kotlin](https://kotlinlang.org/) on Android & [Swift](https://developer.apple.com/swift/) on iOS -- Support for C++ to write cross-platform native code -- [Expo](https://expo.io/) for libraries without native code and web support +- Example apps with [Community CLI](https://github.com/react-native-community/cli), [Expo](https://expo.dev/) or [React Native Test App](https://github.com/microsoft/react-native-test-app) +- Support for Web with [Expo Web](https://docs.expo.dev/workflow/web/) or [Vite](https://vitejs.dev/) - [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/), [Lefthook](https://github.com/evilmartians/lefthook) and [Release It](https://github.com/release-it/release-it) pre-configured - [`react-native-builder-bob`](./build.md) pre-configured to compile your files - [GitHub Actions](https://github.com/features/actions) pre-configured to run tests and lint on the CI diff --git a/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts b/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts index 6acc097b6..a789eb146 100644 --- a/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts +++ b/packages/create-react-native-library/src/exampleApp/generateExampleApp.ts @@ -41,7 +41,6 @@ const PACKAGES_TO_REMOVE = [ const PACKAGES_TO_ADD_EXPO_WEB = { '@expo/metro-runtime': '~5.0.4', - 'react-dom': '19.1.0', 'react-native-web': '~0.21.1', }; @@ -173,9 +172,11 @@ export default async function generateExampleApp({ 'build:ios': `react-native build-ios --mode Debug`, }; - if (config.example === 'vanilla') { + if (config.example != null) { Object.assign(scripts, SCRIPTS_TO_ADD); - } else if (config.example === 'test-app') { + } + + if (config.example === 'test-app') { // `react-native-test-app` doesn't bundle application by default in 'Release' mode and also `bundle` command doesn't create a directory. // `mkdist` script should be removed after stable React Native major contains this fix: https://github.com/facebook/react-native/pull/45182. @@ -276,8 +277,6 @@ export default async function generateExampleApp({ scripts.android = 'expo run:android'; scripts.ios = 'expo run:ios'; - delete scripts.web; - await fs.writeFile( path.join(directory, '.gitignore'), dedent` @@ -286,14 +285,22 @@ export default async function generateExampleApp({ ios/ ` ); - } else { - Object.entries(PACKAGES_TO_ADD_EXPO_WEB).forEach(([name, version]) => { - dependencies[name] = bundledNativeModules[name] || version; - }); + } - scripts.web = 'expo start --web'; + const reactVersion = dependencies.react ?? devDependencies.react; + + if (typeof reactVersion !== 'string') { + throw new Error("Couldn't find the package 'react' in the example app."); } + Object.entries(PACKAGES_TO_ADD_EXPO_WEB).forEach(([name, version]) => { + dependencies[name] = bundledNativeModules[name] || version; + }); + + dependencies['react-dom'] = reactVersion; + scripts.web = 'expo start --web'; + scripts['build:web'] = 'expo export --platform web'; + const app = await fs.readJSON(path.join(directory, 'app.json')); app.expo.name = `${config.project.name} Example`; diff --git a/packages/create-react-native-library/src/inform.ts b/packages/create-react-native-library/src/inform.ts index b7a0fcda6..2079c76a2 100644 --- a/packages/create-react-native-library/src/inform.ts +++ b/packages/create-react-native-library/src/inform.ts @@ -7,7 +7,7 @@ export function printNonLocalLibNextSteps(config: TemplateConfiguration) { const platforms = { ios: { name: 'iOS', color: 'cyan' }, android: { name: 'Android', color: 'green' }, - ...(config.example === 'expo' + ...(config.example === 'expo' || config.tools.includes('vite') ? ({ web: { name: 'Web', color: 'blue' } } as const) : null), } as const; diff --git a/packages/create-react-native-library/src/prompt.ts b/packages/create-react-native-library/src/prompt.ts index ccaf5e523..620c4c531 100644 --- a/packages/create-react-native-library/src/prompt.ts +++ b/packages/create-react-native-library/src/prompt.ts @@ -319,15 +319,6 @@ export const prompt = create(['[name]'], { title: choice.title, value: choice.value, description: choice.description, - skip: (): boolean => { - const answers = prompt.read(); - - if (answers.type === 'library') { - return choice.value !== 'expo'; - } - - return false; - }, })), required: true, skip: (): boolean => { @@ -344,8 +335,27 @@ export const prompt = create(['[name]'], { value: key, title: tool.name, description: tool.description, + skip: (): boolean => { + if ('condition' in tool && tool.condition) { + return !tool.condition({ example: prompt.read().example }); + } + + return false; + }, })), - default: Object.keys(AVAILABLE_TOOLS), + default: (): string[] => { + const answers = prompt.read(); + + return Object.entries(AVAILABLE_TOOLS) + .filter(([, tool]) => { + if ('condition' in tool && tool.condition) { + return tool.condition({ example: answers.example }); + } + + return true; + }) + .map(([key]) => key); + }, required: true, skip: (): boolean => { const answers = prompt.read(); diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index cf065ce2a..d57e23a34 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -56,6 +56,7 @@ const EXAMPLE_COMMON_FILES = path.resolve( __dirname, '../templates/example-common' ); +const EXAMPLE_BARE_FILES = path.resolve(__dirname, '../templates/example-bare'); const EXAMPLE_MODULE_NEW_FILES = path.resolve( __dirname, '../templates/example-module-new' @@ -64,13 +65,14 @@ const EXAMPLE_VIEW_FILES = path.resolve(__dirname, '../templates/example-view'); const EXAMPLE_EXPO_FILES = path.resolve(__dirname, '../templates/example-expo'); const JS_FILES = path.resolve(__dirname, '../templates/js-library'); +const JS_VIEW_FILES = path.resolve(__dirname, '../templates/js-view'); const NATIVE_COMMON_FILES = path.resolve( __dirname, '../templates/native-common' ); -const NATIVE_COMMON_EXAMPLE_FILES = path.resolve( +const EXAMPLE_NATIVE_COMMON_FILES = path.resolve( __dirname, - '../templates/native-common-example' + '../templates/example-native-common' ); const NITRO_COMMON_FILES = path.resolve(__dirname, '../templates/nitro-common'); @@ -203,27 +205,50 @@ export async function applyTemplates( if (answers.languages === 'js') { await applyTemplate(config, JS_FILES, folder); - await applyTemplate(config, EXAMPLE_EXPO_FILES, folder); - } else { - await applyTemplate(config, NATIVE_COMMON_FILES, folder); if (config.example != null) { - await applyTemplate(config, NATIVE_COMMON_EXAMPLE_FILES, folder); + if (config.example === 'expo') { + await applyTemplate(config, EXAMPLE_EXPO_FILES, folder); + } else { + await applyTemplate(config, EXAMPLE_BARE_FILES, folder); + } } + } else { + await applyTemplate(config, NATIVE_COMMON_FILES, folder); if (config.example === 'expo') { await applyTemplate(config, EXAMPLE_EXPO_FILES, folder); + + if (config.project.native) { + await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder); + } + } else if (config.example != null) { + await applyTemplate(config, EXAMPLE_BARE_FILES, folder); + + if (config.project.native) { + await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder); + } } if (config.project.moduleConfig === 'nitro-modules') { await applyTemplate(config, NITRO_COMMON_FILES, folder); await applyTemplate(config, NATIVE_FILES['module_nitro'], folder); + + if (config.example === 'expo' || config.tools.includes('vite')) { + await applyTemplate(config, JS_FILES, folder); + } + return; } if (config.project.viewConfig === 'nitro-view') { await applyTemplate(config, NITRO_COMMON_FILES, folder); await applyTemplate(config, NATIVE_FILES['view_nitro'], folder); + + if (config.example === 'expo' || config.tools.includes('vite')) { + await applyTemplate(config, JS_VIEW_FILES, folder); + } + return; } @@ -244,6 +269,14 @@ export async function applyTemplates( }_new` as const; await applyTemplate(config, KOTLIN_FILES[templateType], folder); + + if (config.example === 'expo' || config.tools.includes('vite')) { + if (config.project.viewConfig !== null) { + await applyTemplate(config, JS_VIEW_FILES, folder); + } else { + await applyTemplate(config, JS_FILES, folder); + } + } } } diff --git a/packages/create-react-native-library/src/utils/configureTools.ts b/packages/create-react-native-library/src/utils/configureTools.ts index 4b8fe139d..b181edc8c 100644 --- a/packages/create-react-native-library/src/utils/configureTools.ts +++ b/packages/create-react-native-library/src/utils/configureTools.ts @@ -3,16 +3,27 @@ import path from 'node:path'; import { applyTemplate, type TemplateConfiguration } from '../template'; import sortObjectKeys from './sortObjectKeys'; +type PackageJson = { + dependencies?: Record; + devDependencies?: Record; + peerDependencies?: Record; + [key: string]: unknown; +}; + type Tool = { name: string; description: string; - condition?: (config: TemplateConfiguration) => boolean; + condition?: (context: Pick) => boolean; + postprocess?: (options: { + config: TemplateConfiguration; + root: string; + }) => void | Promise; }; type Options = { tools: string[]; root: string; - packageJson: Record; + packageJson: PackageJson; config: TemplateConfiguration; }; @@ -41,11 +52,45 @@ const TURBOREPO = { description: 'Cache build outputs on CI', }; +const VITE: Tool = { + name: 'Vite', + description: 'Add web support to the example app', + condition: (config) => config.example != null && config.example !== 'expo', + postprocess: async ({ root }) => { + const examplePkgPath = path.join(root, 'example', 'package.json'); + + if (!fs.existsSync(examplePkgPath)) { + throw new Error("Couldn't find the example app's package.json."); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const examplePackageJson = (await fs.readJson( + examplePkgPath + )) as PackageJson; + + const reactVersion = + examplePackageJson.dependencies?.react ?? + examplePackageJson.devDependencies?.react; + + if (reactVersion == null) { + throw new Error("Couldn't find the package 'react' in the example app."); + } + + examplePackageJson.dependencies = sortObjectKeys({ + ...examplePackageJson.dependencies, + 'react-dom': reactVersion, + }); + + await fs.writeJson(examplePkgPath, examplePackageJson, { spaces: 2 }); + }, +}; + export const AVAILABLE_TOOLS = { eslint: ESLINT, jest: JEST, lefthook: LEFTHOOK, 'release-it': RELEASE_IT, + vite: VITE, } as const satisfies Record; const REQUIRED_TOOLS = { @@ -89,44 +134,73 @@ export async function configureTools({ await applyTemplate(config, toolDir, root); } - const pkgPath = path.join(toolDir, '~package.json'); + const examplePkgPath = path.join(toolDir, 'example', '~package.json'); + + await mergePackageJsonTemplate( + path.join(toolDir, '~package.json'), + packageJson + ); - if (fs.existsSync(pkgPath)) { + if (fs.existsSync(examplePkgPath)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const toolPkg = (await fs.readJson(pkgPath)) as Record; + const existingExamplePackageJson = (await fs.readJson( + path.join(root, 'example', 'package.json') + )) as PackageJson; + + await mergePackageJsonTemplate( + examplePkgPath, + existingExamplePackageJson + ); + + await fs.writeJson( + path.join(root, 'example', 'package.json'), + existingExamplePackageJson, + { + spaces: 2, + } + ); + } + + await tool.postprocess?.({ config, root }); + } +} + +async function mergePackageJsonTemplate( + templatePath: string, + packageJson: PackageJson +) { + if (!fs.existsSync(templatePath)) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const template = (await fs.readJson(templatePath)) as PackageJson; + + for (const [field, value] of Object.entries(template)) { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + if ( + typeof packageJson[field] === 'object' || + packageJson[field] == null + ) { + packageJson[field] = { + ...packageJson[field], + ...value, + }; - for (const [field, value] of Object.entries(toolPkg)) { if ( - typeof value === 'object' && - value !== null && - !Array.isArray(value) + field === 'dependencies' || + field === 'devDependencies' || + field === 'peerDependencies' ) { - if ( - typeof packageJson[field] === 'object' || - packageJson[field] == null - ) { - packageJson[field] = { - ...packageJson[field], - ...value, - }; - - if ( - field === 'dependencies' || - field === 'devDependencies' || - field === 'peerDependencies' - ) { - // @ts-expect-error: We know they are objects here - packageJson[field] = sortObjectKeys(packageJson[field]); - } - } else { - throw new Error( - `Cannot merge '${field}' field because it is not an object (got '${String(packageJson[field])}').` - ); - } - } else { - packageJson[field] = value; + packageJson[field] = sortObjectKeys(packageJson[field]); } + } else { + throw new Error( + `Cannot merge '${field}' field because it is not an object (got '${String(packageJson[field])}').` + ); } + } else { + packageJson[field] = value; } } } diff --git a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml index 15c2f7c21..5225a72d1 100644 --- a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml +++ b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: - name: Build package run: yarn prepare -<% if (project.native) { -%> +<% if (example != null) { -%> build-android: runs-on: ubuntu-latest @@ -109,7 +109,7 @@ jobs: /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" <% if (example === 'expo') { -%> - - name: Prebuild expo app for iOS + - name: Prebuild expo app for Android if: env.turbo_cache_hit != 1 run: | yarn example expo prebuild --platform android @@ -193,7 +193,8 @@ jobs: - name: Build example for iOS run: | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" -<% } else { -%> +<% } -%> +<% if (example === 'expo' || tools.includes('vite')) { -%> build-web: runs-on: ubuntu-latest @@ -206,5 +207,5 @@ jobs: - name: Build example for Web run: | - yarn example expo export --platform web + yarn example build:web <% } -%> diff --git a/packages/create-react-native-library/templates/common/CONTRIBUTING.md b/packages/create-react-native-library/templates/common/CONTRIBUTING.md index a118950bf..78cdab2b4 100644 --- a/packages/create-react-native-library/templates/common/CONTRIBUTING.md +++ b/packages/create-react-native-library/templates/common/CONTRIBUTING.md @@ -76,7 +76,7 @@ Running "<%- project.name -%>Example" with {"fabric":true,"initialProps":{"concu Note the `"fabric":true` and `"concurrentRoot":true` properties. -<% if (!project.native) { -%> +<% if (example === 'expo' || tools.includes('vite')) { -%> To run the example app on Web: ```sh @@ -148,15 +148,19 @@ The `package.json` file contains various scripts for common tasks: - `yarn`: setup project by installing dependencies. - `yarn typecheck`: type-check files with TypeScript. -<% if (tools.includes('eslint')) { -%> + <% if (tools.includes('eslint')) { -%> - `yarn lint`: lint files with [ESLint](https://eslint.org/). -<% } -%> -<% if (tools.includes('jest')) { -%> + <% } -%> + <% if (tools.includes('jest')) { -%> - `yarn test`: run unit tests with [Jest](https://jestjs.io/). -<% } -%> + <% } -%> - `yarn example start`: start the Metro server for the example app. - `yarn example android`: run the example app on Android. - `yarn example ios`: run the example app on iOS. + <% if (example === 'expo' || tools.includes('vite')) { -%> +- `yarn example web`: run the example app on Web. +- `yarn example build:web`: build the example app for Web. + <% } -%> ### Sending a pull request diff --git a/packages/create-react-native-library/templates/common/README.md b/packages/create-react-native-library/templates/common/README.md index 716271bf2..4561a5be7 100644 --- a/packages/create-react-native-library/templates/common/README.md +++ b/packages/create-react-native-library/templates/common/README.md @@ -32,16 +32,6 @@ import { <%- project.name -%>View } from "<%- project.slug -%>"; <<%- project.name -%>View color="tomato" /> ``` -<% } else if (project.moduleConfig === 'nitro-modules' || project.viewConfig === 'nitro-view' || project.moduleConfig === 'turbo-modules') { -%> - -```js -import { multiply } from '<%- project.slug -%>'; - -// ... - -const result = multiply(3, 7); -``` - <% } else { -%> ```js @@ -49,7 +39,7 @@ import { multiply } from '<%- project.slug -%>'; // ... -const result = await multiply(3, 7); +const result = multiply(3, 7); ``` <% } -%> diff --git a/packages/create-react-native-library/templates/native-common-example/example/babel.config.js b/packages/create-react-native-library/templates/example-bare/example/babel.config.js similarity index 100% rename from packages/create-react-native-library/templates/native-common-example/example/babel.config.js rename to packages/create-react-native-library/templates/example-bare/example/babel.config.js diff --git a/packages/create-react-native-library/templates/native-common-example/example/index.js b/packages/create-react-native-library/templates/example-bare/example/index.js similarity index 100% rename from packages/create-react-native-library/templates/native-common-example/example/index.js rename to packages/create-react-native-library/templates/example-bare/example/index.js diff --git a/packages/create-react-native-library/templates/native-common-example/example/react-native.config.js b/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js similarity index 100% rename from packages/create-react-native-library/templates/native-common-example/example/react-native.config.js rename to packages/create-react-native-library/templates/example-native-common/example/react-native.config.js diff --git a/packages/create-react-native-library/templates/js-library/src/index.tsx b/packages/create-react-native-library/templates/js-library/src/index.tsx index 14289fe2e..da87a2292 100644 --- a/packages/create-react-native-library/templates/js-library/src/index.tsx +++ b/packages/create-react-native-library/templates/js-library/src/index.tsx @@ -1,3 +1 @@ -export function multiply(a: number, b: number): number { - return a * b; -} +export { multiply } from './multiply'; diff --git a/packages/create-react-native-library/templates/js-library/src/multiply.tsx b/packages/create-react-native-library/templates/js-library/src/multiply.tsx new file mode 100644 index 000000000..14289fe2e --- /dev/null +++ b/packages/create-react-native-library/templates/js-library/src/multiply.tsx @@ -0,0 +1,3 @@ +export function multiply(a: number, b: number): number { + return a * b; +} diff --git a/packages/create-react-native-library/templates/js-view/src/index.tsx b/packages/create-react-native-library/templates/js-view/src/index.tsx new file mode 100644 index 000000000..351349888 --- /dev/null +++ b/packages/create-react-native-library/templates/js-view/src/index.tsx @@ -0,0 +1 @@ +export { <%- project.name -%>View } from './<%- project.name -%>View'; diff --git a/packages/create-react-native-library/templates/js-view/src/{%- project.name %}View.tsx b/packages/create-react-native-library/templates/js-view/src/{%- project.name %}View.tsx new file mode 100644 index 000000000..7ffb3bdb6 --- /dev/null +++ b/packages/create-react-native-library/templates/js-view/src/{%- project.name %}View.tsx @@ -0,0 +1,13 @@ +import { View, type ColorValue, type ViewProps } from 'react-native'; + +type Props = ViewProps & { + color?: ColorValue; +}; + +export function <%- project.name -%>View({ + color, + style, + ...rest +}: Props) { + return ; +} diff --git a/packages/create-react-native-library/templates/native-library-new/src/index.tsx b/packages/create-react-native-library/templates/native-library-new/src/index.tsx index 18177d61f..da87a2292 100644 --- a/packages/create-react-native-library/templates/native-library-new/src/index.tsx +++ b/packages/create-react-native-library/templates/native-library-new/src/index.tsx @@ -1,5 +1 @@ -import <%- project.name -%> from './Native<%- project.name -%>'; - -export function multiply(a: number, b: number): number { - return <%- project.name -%>.multiply(a, b); -} +export { multiply } from './multiply'; diff --git a/packages/create-react-native-library/templates/native-library-new/src/multiply.native.tsx b/packages/create-react-native-library/templates/native-library-new/src/multiply.native.tsx new file mode 100644 index 000000000..18177d61f --- /dev/null +++ b/packages/create-react-native-library/templates/native-library-new/src/multiply.native.tsx @@ -0,0 +1,5 @@ +import <%- project.name -%> from './Native<%- project.name -%>'; + +export function multiply(a: number, b: number): number { + return <%- project.name -%>.multiply(a, b); +} diff --git a/packages/create-react-native-library/templates/native-library-new/src/multiply.tsx b/packages/create-react-native-library/templates/native-library-new/src/multiply.tsx new file mode 100644 index 000000000..74ed734c9 --- /dev/null +++ b/packages/create-react-native-library/templates/native-library-new/src/multiply.tsx @@ -0,0 +1,5 @@ +export function multiply(_a: number, _b: number): number { + throw new Error( + "'<%- project.slug -%>' is only supported on native platforms." + ); +} diff --git a/packages/create-react-native-library/templates/native-view-new/src/index.tsx b/packages/create-react-native-library/templates/native-view-new/src/index.tsx index 7a2bfbdc2..351349888 100644 --- a/packages/create-react-native-library/templates/native-view-new/src/index.tsx +++ b/packages/create-react-native-library/templates/native-view-new/src/index.tsx @@ -1,2 +1 @@ -export { default as <%- project.name -%>View } from './<%- project.name -%>ViewNativeComponent'; -export * from './<%- project.name -%>ViewNativeComponent'; +export { <%- project.name -%>View } from './<%- project.name -%>View'; diff --git a/packages/create-react-native-library/templates/native-view-new/src/{%- project.name %}View.native.tsx b/packages/create-react-native-library/templates/native-view-new/src/{%- project.name %}View.native.tsx new file mode 100644 index 000000000..7a2bfbdc2 --- /dev/null +++ b/packages/create-react-native-library/templates/native-view-new/src/{%- project.name %}View.native.tsx @@ -0,0 +1,2 @@ +export { default as <%- project.name -%>View } from './<%- project.name -%>ViewNativeComponent'; +export * from './<%- project.name -%>ViewNativeComponent'; diff --git a/packages/create-react-native-library/templates/native-view-new/src/{%- project.name %}View.tsx b/packages/create-react-native-library/templates/native-view-new/src/{%- project.name %}View.tsx new file mode 100644 index 000000000..77709ebb1 --- /dev/null +++ b/packages/create-react-native-library/templates/native-view-new/src/{%- project.name %}View.tsx @@ -0,0 +1,11 @@ +import type { ColorValue, ViewProps } from 'react-native'; + +type Props = ViewProps & { + color?: ColorValue; +}; + +export function <%- project.name -%>View(_props: Props): never { + throw new Error( + "'<%- project.slug -%>' is only supported on native platforms." + ); +} diff --git a/packages/create-react-native-library/templates/nitro-common/nitro.json b/packages/create-react-native-library/templates/nitro-common/nitro.json index f4b2ecd63..c7e079098 100644 --- a/packages/create-react-native-library/templates/nitro-common/nitro.json +++ b/packages/create-react-native-library/templates/nitro-common/nitro.json @@ -9,8 +9,14 @@ }, "autolinking": { "<%- project.name -%>": { - "swift": "<% if (project.viewConfig === 'nitro-view') { -%>Hybrid<% } -%><%- project.name -%>", - "kotlin": "<% if (project.viewConfig === 'nitro-view') { -%>Hybrid<% } -%><%- project.name -%>" + "ios": { + "language": "swift", + "implementationClassName": "<% if (project.viewConfig === 'nitro-view') { -%>Hybrid<% } -%><%- project.name -%>" + }, + "android": { + "language": "kotlin", + "implementationClassName": "<% if (project.viewConfig === 'nitro-view') { -%>Hybrid<% } -%><%- project.name -%>" + } } }, "ignorePaths": ["node_modules"] diff --git a/packages/create-react-native-library/templates/nitro-module/src/index.tsx b/packages/create-react-native-library/templates/nitro-module/src/index.tsx index fb7633d49..da87a2292 100644 --- a/packages/create-react-native-library/templates/nitro-module/src/index.tsx +++ b/packages/create-react-native-library/templates/nitro-module/src/index.tsx @@ -1,9 +1 @@ -import { NitroModules } from 'react-native-nitro-modules'; -import type { <%- project.name -%> } from './<%- project.name -%>.nitro'; - -const <%- project.name -%>HybridObject = - NitroModules.createHybridObject<<%- project.name -%>>('<%- project.name -%>'); - -export function multiply(a: number, b: number): number { - return <%- project.name -%>HybridObject.multiply(a, b); -} +export { multiply } from './multiply'; diff --git a/packages/create-react-native-library/templates/nitro-module/src/multiply.native.tsx b/packages/create-react-native-library/templates/nitro-module/src/multiply.native.tsx new file mode 100644 index 000000000..fb7633d49 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-module/src/multiply.native.tsx @@ -0,0 +1,9 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import type { <%- project.name -%> } from './<%- project.name -%>.nitro'; + +const <%- project.name -%>HybridObject = + NitroModules.createHybridObject<<%- project.name -%>>('<%- project.name -%>'); + +export function multiply(a: number, b: number): number { + return <%- project.name -%>HybridObject.multiply(a, b); +} diff --git a/packages/create-react-native-library/templates/nitro-module/src/multiply.tsx b/packages/create-react-native-library/templates/nitro-module/src/multiply.tsx new file mode 100644 index 000000000..74ed734c9 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-module/src/multiply.tsx @@ -0,0 +1,5 @@ +export function multiply(_a: number, _b: number): number { + throw new Error( + "'<%- project.slug -%>' is only supported on native platforms." + ); +} diff --git a/packages/create-react-native-library/templates/nitro-view/src/index.tsx b/packages/create-react-native-library/templates/nitro-view/src/index.tsx index 2f1c95268..351349888 100644 --- a/packages/create-react-native-library/templates/nitro-view/src/index.tsx +++ b/packages/create-react-native-library/templates/nitro-view/src/index.tsx @@ -1,11 +1 @@ -import { getHostComponent } from 'react-native-nitro-modules'; -const <%- project.name %>Config = require('../nitrogen/generated/shared/json/<%- project.name %>Config.json'); -import type { - <%- project.name %>Methods, - <%- project.name %>Props, -} from './<%- project.name %>.nitro'; - -export const <%- project.name %>View = getHostComponent< - <%- project.name %>Props, - <%- project.name %>Methods ->('<%- project.name %>', () => <%- project.name %>Config); +export { <%- project.name -%>View } from './<%- project.name -%>View'; diff --git a/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}View.native.tsx b/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}View.native.tsx new file mode 100644 index 000000000..2f1c95268 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}View.native.tsx @@ -0,0 +1,11 @@ +import { getHostComponent } from 'react-native-nitro-modules'; +const <%- project.name %>Config = require('../nitrogen/generated/shared/json/<%- project.name %>Config.json'); +import type { + <%- project.name %>Methods, + <%- project.name %>Props, +} from './<%- project.name %>.nitro'; + +export const <%- project.name %>View = getHostComponent< + <%- project.name %>Props, + <%- project.name %>Methods +>('<%- project.name %>', () => <%- project.name %>Config); diff --git a/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}View.tsx b/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}View.tsx new file mode 100644 index 000000000..77709ebb1 --- /dev/null +++ b/packages/create-react-native-library/templates/nitro-view/src/{%- project.name %}View.tsx @@ -0,0 +1,11 @@ +import type { ColorValue, ViewProps } from 'react-native'; + +type Props = ViewProps & { + color?: ColorValue; +}; + +export function <%- project.name -%>View(_props: Props): never { + throw new Error( + "'<%- project.slug -%>' is only supported on native platforms." + ); +} diff --git a/packages/create-react-native-library/templates/tools/vite/example/index.html b/packages/create-react-native-library/templates/tools/vite/example/index.html new file mode 100644 index 000000000..922fd80af --- /dev/null +++ b/packages/create-react-native-library/templates/tools/vite/example/index.html @@ -0,0 +1,25 @@ + + + + + + + + +
+ + + diff --git a/packages/create-react-native-library/templates/tools/vite/example/index.js b/packages/create-react-native-library/templates/tools/vite/example/index.js new file mode 100644 index 000000000..b628f37ff --- /dev/null +++ b/packages/create-react-native-library/templates/tools/vite/example/index.js @@ -0,0 +1,11 @@ +import { AppRegistry } from 'react-native'; +import App from './src/App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); + +if (typeof document !== 'undefined') { + AppRegistry.runApplication(appName, { + rootTag: document.getElementById('root'), + }); +} diff --git a/packages/create-react-native-library/templates/tools/vite/example/vite.config.mjs b/packages/create-react-native-library/templates/tools/vite/example/vite.config.mjs new file mode 100644 index 000000000..6b568a4b3 --- /dev/null +++ b/packages/create-react-native-library/templates/tools/vite/example/vite.config.mjs @@ -0,0 +1,15 @@ +import { fileURLToPath } from 'node:url'; + +import { defineConfig, mergeConfig } from 'vite'; + +import bobConfig from 'react-native-builder-bob/vite-config.mjs'; + +export default defineConfig((env) => + mergeConfig(bobConfig(env), { + resolve: { + alias: { + '<%- project.slug -%>': fileURLToPath(new URL('..', import.meta.url)), + }, + }, + }) +); diff --git a/packages/create-react-native-library/templates/tools/vite/example/~package.json b/packages/create-react-native-library/templates/tools/vite/example/~package.json new file mode 100644 index 000000000..77737534a --- /dev/null +++ b/packages/create-react-native-library/templates/tools/vite/example/~package.json @@ -0,0 +1,14 @@ +{ + "scripts": { + "web": "vite", + "build:web": "vite build" + }, + "dependencies": { + "react-native-web": "~0.21.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.0", + "vite": "^7.1.1", + "vite-plugin-commonjs": "^0.10.4" + } +} diff --git a/packages/react-native-builder-bob/package.json b/packages/react-native-builder-bob/package.json index c247ebf2f..f572f7c87 100644 --- a/packages/react-native-builder-bob/package.json +++ b/packages/react-native-builder-bob/package.json @@ -26,7 +26,8 @@ "lib", "babel-preset.js", "metro-config.js", - "babel-config.js" + "babel-config.js", + "vite-config.mjs" ], "engines": { "node": "^20.19.0 || ^22.12.0 || >= 23.4.0" diff --git a/packages/react-native-builder-bob/vite-config.mjs b/packages/react-native-builder-bob/vite-config.mjs new file mode 100644 index 000000000..6f4d215b0 --- /dev/null +++ b/packages/react-native-builder-bob/vite-config.mjs @@ -0,0 +1,65 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig, transformWithEsbuild } from 'vite'; +import commonjs from 'vite-plugin-commonjs'; + +const extensions = [ + '.web.tsx', + '.tsx', + '.web.ts', + '.ts', + '.web.jsx', + '.jsx', + '.web.js', + '.js', + '.web.mjs', + '.mjs', + '.json', +]; + +/** + * @param {RegExp} regex + * @returns {import('vite').Plugin} + */ +const jsx = (regex) => ({ + name: 'js-as-jsx', + enforce: 'pre', + transform(code, id) { + if (id.endsWith('.js') && regex.test(id)) { + return transformWithEsbuild(code, id, { + loader: 'jsx', + jsx: 'automatic', + }); + } + + return null; + }, +}); + +export default defineConfig(({ mode }) => ({ + plugins: [ + jsx(/\/(@expo|expo-.+)\//), + commonjs(), + react(), + ], + define: { + '__DEV__': JSON.stringify(mode !== 'production'), + 'process.env.EXPO_OS': JSON.stringify('web'), + 'global': 'globalThis', + }, + resolve: { + extensions, + conditions: ['source', 'module', 'browser', mode], + alias: { + 'react-native': 'react-native-web', + }, + }, + optimizeDeps: { + esbuildOptions: { + resolveExtensions: extensions, + jsx: 'automatic', + loader: { + '.js': 'jsx', + }, + }, + }, +})); diff --git a/scripts/upgrade-template-deps.mts b/scripts/upgrade-template-deps.mts index 27de68a8a..2cdf69bad 100755 --- a/scripts/upgrade-template-deps.mts +++ b/scripts/upgrade-template-deps.mts @@ -57,7 +57,10 @@ function getTemplatePackageFiles() { path.join(TEMPLATES_DIR, 'common', '$package.json'), ...fs .readdirSync(path.join(TEMPLATES_DIR, 'tools')) - .map((dir) => path.join(TEMPLATES_DIR, 'tools', dir, '~package.json')) + .flatMap((dir) => [ + path.join(TEMPLATES_DIR, 'tools', dir, '~package.json'), + path.join(TEMPLATES_DIR, 'tools', dir, 'example', '~package.json'), + ]) .filter((filePath) => fs.existsSync(filePath)), ]; }