diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6afa2392d..3a5b03ece 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ And then: ```sh cd /my/new/react-native/project/ -yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-clean" "@react-native-community/cli-doctor" "@react-native-community/cli-config" "@react-native-community/cli-platform-apple" +yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-clean" "@react-native-community/cli-doctor" "@react-native-community/cli-config" "@react-native-community/cli-platform-apple" "@react-native-community/cli-link-assets" ``` Once you're done with testing and you'd like to get back to regular setup, run `yarn unlink` instead of `yarn link` from above command. Then `yarn install --force`. diff --git a/README.md b/README.md index d1518b8c6..18a9da6ed 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ React Native CLI is a dependency of `react-native`, which makes it a transitive "@react-native-community/cli-config": "VERSION", "@react-native-community/cli-debugger-ui": "VERSION", "@react-native-community/cli-doctor": "VERSION", + "@react-native-community/cli-link-assets": "VERSION", "@react-native-community/cli-platform-android": "VERSION", "@react-native-community/cli-platform-ios": "VERSION", "@react-native-community/cli-server-api": "VERSION", diff --git a/__e2e__/__snapshots__/config.test.ts.snap b/__e2e__/__snapshots__/config.test.ts.snap index 4d0436496..de4d477f0 100644 --- a/__e2e__/__snapshots__/config.test.ts.snap +++ b/__e2e__/__snapshots__/config.test.ts.snap @@ -79,16 +79,19 @@ exports[`shows up current config without unnecessary output 1`] = ` "ios": {}, "android": {} }, + "assets": [], "project": { "ios": { - "sourceDir": "<>/TestProject/ios" + "sourceDir": "<>/TestProject/ios", + "assets": [] }, "android": { "sourceDir": "<>/TestProject/android", "appName": "app", "packageName": "com.testproject", "applicationId": "com.testproject", - "mainActivity": ".MainActivity" + "mainActivity": ".MainActivity", + "assets": [] } } } diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index 1a5cd8681..da7a7543b 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -79,10 +79,12 @@ test('shows up current config without unnecessary output', () => { ? { name: 'TestProject.xcworkspace', isWorkspace: true, + path: '.', } : { name: 'TestProject.xcodeproj', isWorkspace: false, + path: '.', }; expect(parsedStdout.project.ios.xcodeProject).toStrictEqual( diff --git a/docs/commands.md b/docs/commands.md index eccbff0b1..4316e2cde 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -6,6 +6,7 @@ React Native CLI comes with following commands: - [`clean`](/packages/cli-clean/README.md#clean) - [`config`](/packages/cli-config/README.md#config) - [`doctor`](/packages/cli-doctor/README.md#doctor) +- [`link-assets`](/packages/cli-link-assets/README.md#link-assets) - [`init`](#init) - [`info`](/packages/cli-doctor/README.md#info) - [`log-android`](/packages/cli-platform-android/README.md#log-android) diff --git a/docs/projects.md b/docs/projects.md index 0074b987e..580707be3 100644 --- a/docs/projects.md +++ b/docs/projects.md @@ -47,6 +47,7 @@ type ProjectConfigT = { [key: string]: DependencyConfig; }; commands: Command[]; + assets?: string[]; }; ``` @@ -69,6 +70,7 @@ type IOSProjectParams = { sourceDir?: string; watchModeCommandParams?: string[]; automaticPodsInstallation?: boolean; + assets?: string[]; }; type AndroidProjectParams = { @@ -78,6 +80,7 @@ type AndroidProjectParams = { packageName?: string; dependencyConfiguration?: string; watchModeCommandParams?: string[]; + assets?: string[]; }; ``` @@ -116,6 +119,10 @@ If set to `true`, you can skip running `pod install` manually whenever it's need > Note: Starting from React Native 0.73, CLI's `init` command scaffolds the project with `react-native.config.js` file with this value set to `true` by default. Older projects can opt-in after migrating to 0.73. Please note that if your setup does not follow the standard React Native template, e.g. you are not using Gems to install CocoaPods, this might not work properly for you. +### project.ios.assets + +Array of folder paths that will be passed to the `npx react-native link-assets` command to specify the assets to be linked to iOS project. + #### project.android.appName A name of the app in the Android `sourceDir`, equivalent to Gradle project name. By default it's `app`. @@ -155,6 +162,10 @@ The list should contain the name of the components, as they're registered in the Since React Native 0.74, this property is ignored as the Interop Layer is **Automatic**, you don't need to register the Legacy Components anymore and they will be discovered automatically. +### project.android.assets + +Array of folder paths that will be passed to the `npx react-native link-assets` command to specify the assets to be linked to Android project. + ### platforms A object with platforms defined inside a project. You can check the format and options available [`here`](platforms.md#platform-interface) @@ -220,3 +231,7 @@ module.exports = { The object provided here is deep merged with the dependency config. Check [`projectConfig`](platforms.md#projectconfig) and [`dependencyConfig`](platforms.md#dependencyConfig) return values for a full list of properties that you can override. > Note: This is an advanced feature and you should not need to use it most of the time. + +### assets + +Array of folder paths that will be passed to the `npx react-native link-assets` command to specify the assets to be linked to Android / iOS projects. diff --git a/jest/helpers.ts b/jest/helpers.ts index ba7ee701d..4eda26e2f 100644 --- a/jest/helpers.ts +++ b/jest/helpers.ts @@ -60,7 +60,7 @@ export const cleanup = (directory: string) => { */ export const writeFiles = ( directory: string, - files: {[filename: string]: string}, + files: {[filename: string]: string | NodeJS.ArrayBufferView}, ) => { createDirectory(directory); Object.keys(files).forEach((fileOrPath) => { diff --git a/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap b/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap index 7e0d70fd5..d5720cfa2 100644 --- a/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap +++ b/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap @@ -33,6 +33,7 @@ Object { exports[`should have a valid structure by default 1`] = ` Object { + "assets": Array [], "commands": Array [], "dependencies": Object {}, "healthChecks": Array [], diff --git a/packages/cli-config/src/loadConfig.ts b/packages/cli-config/src/loadConfig.ts index 9886c9acd..cd75733a3 100644 --- a/packages/cli-config/src/loadConfig.ts +++ b/packages/cli-config/src/loadConfig.ts @@ -104,6 +104,7 @@ function loadConfig(projectRoot: string = findProjectRoot()): Config { commands: userConfig.commands, healthChecks: userConfig.healthChecks || [], platforms: userConfig.platforms, + assets: userConfig.assets, get project() { if (lazyProject) { return lazyProject; diff --git a/packages/cli-config/src/schema.ts b/packages/cli-config/src/schema.ts index 681a6692f..6f4e1ba03 100644 --- a/packages/cli-config/src/schema.ts +++ b/packages/cli-config/src/schema.ts @@ -161,6 +161,7 @@ export const projectConfig = t .items(t.string()) .optional(), automaticPodsInstallation: t.bool().default(false), + assets: t.array().items(t.string()).default([]), }) .default({}), android: t @@ -177,6 +178,7 @@ export const projectConfig = t .array() .items(t.string()) .optional(), + assets: t.array().items(t.string()).default([]), }) .default({}), }) diff --git a/packages/cli-link-assets/README.md b/packages/cli-link-assets/README.md new file mode 100644 index 000000000..d57048310 --- /dev/null +++ b/packages/cli-link-assets/README.md @@ -0,0 +1,109 @@ +# @react-native-community/cli-link-assets + +This package is part of the [React Native CLI](../../README.md). It contains commands to link assets to your Android / iOS projects. + +## Installation + +```sh +yarn add @react-native-community/cli-link-assets +``` + +## Commands + +### `link-assets` + +Usage: + +```sh +npx react-native link-assets +``` + +Links your assets to the Android / iOS projects. You must configure your `react.native.config.js` file to specify where your assets are located, e.g: + +```js +module.exports = { + // If you want to link assets to both platforms. + assets: ['./assets/shared'], + project: { + android: { + // If you want to link assets only to Android. + assets: ['./assets/android'], + }, + ios: { + // If you want to link assets only to iOS. + assets: ['./assets/ios'], + automaticPodsInstallation: true, + }, + }, +}; +``` + +### Android + +For **Android**, it supports the linking of the following assets: + +#### Fonts (OTF, TTF) + +Font assets are linked in Android by using [XML resources](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml). For instance, if you add the **Lato** font to your project, it will generate a `lato.xml` file in `android/app/src/main/res/font/` folder with all the font variants that you added. It will also add a method call in `MainApplication.kt` or `MainApplication.java` file in order to register the custom font during the app initialization. It will look something like this: + +```kotlin +// other imports + +import com.facebook.react.common.assets.ReactFontManager // <- imports ReactFontManager. + +class MainApplication : Application(), ReactApplication { + + // other methods + + override fun onCreate() { + super.onCreate() + ReactFontManager.getInstance().addCustomFont(this, "Lato", R.font.lato) // <- registers the custom font. + // ... + } +} +``` + +In this case, `Lato` is what you have to set in the `fontFamily` style of your `Text` component. To select the font variant e.g. weight and style, use `fontWeight` and `fontStyle` styles respectively. + +```jsx +Lato Bold Italic +``` + +> [!IMPORTANT] +> If you have already linked font assets in your Android project with [react-native-asset](https://github.com/unimonkiez/react-native-asset), when running this tool it will relink your fonts to use XML resources as described above. **This migration will allow you to use your fonts in the code the same way you would use it for iOS**. Please update your code to use `fontFamily`, `fontWeight` and `fontStyle` styles correctly. + +#### Images (JPG, PNG, GIF) + +Image assets are linked by copying them to `android/app/src/main/res/drawable/` folder. This can be useful in brownfield applications where you need to use assets in the native side. + +#### Sounds (MP3) + +Sound assets are linked by copying them to `android/app/src/main/res/raw/` folder. This can be useful if used together with [react-native-sound](https://github.com/zmxv/react-native-sound) or in brownfield applications where you need to use assets in the native side. + +#### Others + +Any other custom assets are linked by copying them to `android/app/src/main/assets/custom/` folder. + +### iOS + +For **iOS**, it supports the linking of the following assets: + +#### Fonts (OTF, TTF) + +Font assets are linked in iOS by editing `project.pbxproj` and `Info.plist` files. To use the font in your app, you can a combination of `fontFamily`, `fontWeight` and `fontStyle` styles in the same way you would use for Android. In case you didn't link your font assets in Android and you are not sure which value you have to set in `fontFamily` style, you can use `Font Book` app in your Mac to find out the correct value by looking the `Family Name` property. + +#### Images (JPG, PNG, GIF) + +Image assets are linked by editing `project.pbxproj` and adding them as Resources. This can be useful in brownfield applications where you need to use assets in the native side. + +#### Sounds (MP3) + +Image assets are linked by editing `project.pbxproj` and adding them as Resources. This can be useful if used together with [react-native-sound](https://github.com/zmxv/react-native-sound) or in brownfield applications where you need to use assets in the native side. + +#### Others + +Image assets are linked by editing `project.pbxproj` and adding them as Resources. + +### Manifest files + +In both platforms, linked assets are tracked using the `link-assets-manifest.json` files which are stored in both `android/` and `ios/` folders. **They are necessary to track which assets are currently linked, and if the tool needs to add new ones or remove old assets during linking process, so make sure to commit them!** diff --git a/packages/cli-link-assets/package.json b/packages/cli-link-assets/package.json new file mode 100644 index 000000000..6482581f5 --- /dev/null +++ b/packages/cli-link-assets/package.json @@ -0,0 +1,39 @@ +{ + "name": "@react-native-community/cli-link-assets", + "version": "14.0.0-alpha.2", + "license": "MIT", + "main": "build/index.js", + "publishConfig": { + "access": "public" + }, + "types": "build/index.d.ts", + "dependencies": { + "@react-native-community/cli-config": "14.0.0-alpha.2", + "@react-native-community/cli-platform-android": "14.0.0-alpha.2", + "@react-native-community/cli-platform-apple": "14.0.0-alpha.2", + "@react-native-community/cli-platform-ios": "14.0.0-alpha.2", + "@react-native-community/cli-tools": "14.0.0-alpha.2", + "chalk": "^4.1.2", + "fast-xml-parser": "^4.3.2", + "opentype.js": "^1.3.4", + "plist": "^3.1.0", + "xcode": "^3.0.1" + }, + "files": [ + "build", + "!*.d.ts", + "!*.map" + ], + "devDependencies": { + "@react-native-community/cli-types": "14.0.0-alpha.2", + "@types/opentype.js": "^1.3.8", + "@types/plist": "^3.0.5", + "type-fest": "^4.10.2" + }, + "homepage": "https://github.com/react-native-community/cli/tree/main/packages/cli-link-assets", + "repository": { + "type": "git", + "url": "https://github.com/react-native-community/cli.git", + "directory": "packages/cli-link-assets" + } +} diff --git a/packages/cli-link-assets/src/__fixtures__/files/FiraCode-Bold.otf b/packages/cli-link-assets/src/__fixtures__/files/FiraCode-Bold.otf new file mode 100644 index 000000000..f2c65f6d1 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/FiraCode-Bold.otf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/FiraCode-Regular.otf b/packages/cli-link-assets/src/__fixtures__/files/FiraCode-Regular.otf new file mode 100644 index 000000000..41d6f17ef Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/FiraCode-Regular.otf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/GIF Image.gif b/packages/cli-link-assets/src/__fixtures__/files/GIF Image.gif new file mode 100644 index 000000000..e69de29bb diff --git a/packages/cli-link-assets/src/__fixtures__/files/Info.plist b/packages/cli-link-assets/src/__fixtures__/files/Info.plist new file mode 100644 index 000000000..0382b8275 --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + RN0730 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/cli-link-assets/src/__fixtures__/files/JPG Image.jpg b/packages/cli-link-assets/src/__fixtures__/files/JPG Image.jpg new file mode 100644 index 000000000..a1fcc0321 --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/JPG Image.jpg @@ -0,0 +1 @@ +image_jpg.jpg \ No newline at end of file diff --git a/packages/cli-link-assets/src/__fixtures__/files/Lato-Bold.ttf b/packages/cli-link-assets/src/__fixtures__/files/Lato-Bold.ttf new file mode 100644 index 000000000..016068b48 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/Lato-Bold.ttf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/Lato-BoldItalic.ttf b/packages/cli-link-assets/src/__fixtures__/files/Lato-BoldItalic.ttf new file mode 100644 index 000000000..a05d50320 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/Lato-BoldItalic.ttf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/Lato-Light.ttf b/packages/cli-link-assets/src/__fixtures__/files/Lato-Light.ttf new file mode 100644 index 000000000..dfa72ce80 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/Lato-Light.ttf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/Lato-Regular.ttf b/packages/cli-link-assets/src/__fixtures__/files/Lato-Regular.ttf new file mode 100644 index 000000000..bb2e8875a Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/Lato-Regular.ttf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/MP3 Sound (1).mp3 b/packages/cli-link-assets/src/__fixtures__/files/MP3 Sound (1).mp3 new file mode 100644 index 000000000..0fee2fb1d --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/MP3 Sound (1).mp3 @@ -0,0 +1 @@ +sound.mp3 \ No newline at end of file diff --git a/packages/cli-link-assets/src/__fixtures__/files/MainApplication.java b/packages/cli-link-assets/src/__fixtures__/files/MainApplication.java new file mode 100644 index 000000000..2618e588f --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/MainApplication.java @@ -0,0 +1,12 @@ +package com.example; + +import android.app.Application; +import com.facebook.react.ReactApplication; + +public class MainApplication extends Application implements ReactApplication { + @Override + public void onCreate() { + super.onCreate(); + SoLoader.init(this, /* native exopackage */ false); + } +} diff --git a/packages/cli-link-assets/src/__fixtures__/files/MainApplication.kt b/packages/cli-link-assets/src/__fixtures__/files/MainApplication.kt new file mode 100644 index 000000000..8b386d7c4 --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/MainApplication.kt @@ -0,0 +1,11 @@ +package com.example + +import android.app.Application +import com.facebook.react.ReactApplication + +class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + SoLoader.init(this, false) + } +} diff --git a/packages/cli-link-assets/src/__fixtures__/files/Montserrat-Regular.ttf b/packages/cli-link-assets/src/__fixtures__/files/Montserrat-Regular.ttf new file mode 100644 index 000000000..f4a266dd3 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/Montserrat-Regular.ttf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/PNG Image.png b/packages/cli-link-assets/src/__fixtures__/files/PNG Image.png new file mode 100644 index 000000000..2e1760f78 --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/PNG Image.png @@ -0,0 +1 @@ +image_png.png \ No newline at end of file diff --git a/packages/cli-link-assets/src/__fixtures__/files/Raleway-Regular.ttf b/packages/cli-link-assets/src/__fixtures__/files/Raleway-Regular.ttf new file mode 100644 index 000000000..5e7242b83 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/Raleway-Regular.ttf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/TestSample Document.pdf b/packages/cli-link-assets/src/__fixtures__/files/TestSample Document.pdf new file mode 100644 index 000000000..f975f34a8 Binary files /dev/null and b/packages/cli-link-assets/src/__fixtures__/files/TestSample Document.pdf differ diff --git a/packages/cli-link-assets/src/__fixtures__/files/project.pbxproj b/packages/cli-link-assets/src/__fixtures__/files/project.pbxproj new file mode 100644 index 000000000..67b6ad785 --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/files/project.pbxproj @@ -0,0 +1,698 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 00E356F31AD99517003FC87E /* Example.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* Example.m */; }; + 0C80B921A6F3F58F76C31292 /* libPods-Example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-Example.a */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 7699B88040F8A987B510C191 /* libPods-Example-Example.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-Example-Example.a */; }; + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 00E356EE1AD99517003FC87E /* Example.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Example.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* Example.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Example.m; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Example/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Example/AppDelegate.mm; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Example/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Example/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Example/main.m; sourceTree = ""; }; + 19F6CBCC0A4E27FBF8BF4A61 /* libPods-Example-Example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Example-Example.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B4392A12AC88292D35C810B /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = ""; }; + 5709B34CF0A7D63546082F79 /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = ""; }; + 5B7EB9410499542E8C5724F5 /* Pods-Example-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example-Example/Pods-Example-Example.debug.xcconfig"; sourceTree = ""; }; + 5DCACB8F33CDC322A6C60F78 /* libPods-Example.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Example.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Example/LaunchScreen.storyboard; sourceTree = ""; }; + 89C6BE57DB24E9ADA2F236DE /* Pods-Example-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example-Example.release.xcconfig"; path = "Target Support Files/Pods-Example-Example/Pods-Example-Example.release.xcconfig"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7699B88040F8A987B510C191 /* libPods-Example-Example.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0C80B921A6F3F58F76C31292 /* libPods-Example.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00E356EF1AD99517003FC87E /* Example */ = { + isa = PBXGroup; + children = ( + 00E356F21AD99517003FC87E /* Example.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = Example; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* Example */ = { + isa = PBXGroup; + children = ( + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = Example; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 5DCACB8F33CDC322A6C60F78 /* libPods-Example.a */, + 19F6CBCC0A4E27FBF8BF4A61 /* libPods-Example-Example.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* Example */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* Example */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + BBD78D7AC51CEA395F1C20DB /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* Example.app */, + 00E356EE1AD99517003FC87E /* Example.xctest */, + ); + name = Products; + sourceTree = ""; + }; + BBD78D7AC51CEA395F1C20DB /* Pods */ = { + isa = PBXGroup; + children = ( + 3B4392A12AC88292D35C810B /* Pods-Example.debug.xcconfig */, + 5709B34CF0A7D63546082F79 /* Pods-Example.release.xcconfig */, + 5B7EB9410499542E8C5724F5 /* Pods-Example-Example.debug.xcconfig */, + 89C6BE57DB24E9ADA2F236DE /* Pods-Example-Example.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */, + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */, + F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = Example; + productName = Example; + productReference = 00E356EE1AD99517003FC87E /* Example.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 13B07F961A680F5B00A75B9A /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1210; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1120; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* Example */, + 00E356ED1AD99517003FC87E /* Example */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + }; + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + A55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Example-Example-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Example-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example-Example/Pods-Example-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example-Example/Pods-Example-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example-Example/Pods-Example-Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + F6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example-Example/Pods-Example-Example-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example-Example/Pods-Example-Example-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example-Example/Pods-Example-Example-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* Example.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* Example */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-Example-Example.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Example/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-Example-Example.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + INFOPLIST_FILE = Example/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-Example.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = Example; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-Example.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = Example; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.4; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/packages/cli-link-assets/src/__fixtures__/projects.ts b/packages/cli-link-assets/src/__fixtures__/projects.ts new file mode 100644 index 000000000..013329e86 --- /dev/null +++ b/packages/cli-link-assets/src/__fixtures__/projects.ts @@ -0,0 +1,104 @@ +import type FS from 'fs'; +import type Path from 'path'; + +const fs = jest.requireActual('fs'); +const path = jest.requireActual('path'); + +const fixtureFilePaths = { + mainApplicationKotlin: + 'android/app/src/main/java/com/example/MainApplication.kt', + mainApplicationJava: + 'android/app/src/main/java/com/example/MainApplication.java', + infoPlist: 'ios/Example/Info.plist', + projectPbxproj: 'ios/Example.xcodeproj/project.pbxproj', + latoBoldFont: 'assets/android/fonts/Lato-Bold.ttf', + latoBoldItalicFont: 'assets/android/fonts/Lato-BoldItalic.ttf', + montserratRegularFont: 'assets/android/fonts/Montserrat-Regular.ttf', + ralewayRegularFont: 'assets/ios/fonts/Raleway-Regular.ttf', + firaCodeBoldFont: 'assets/shared/fonts/FiraCode-Bold.otf', + firaCodeRegularFont: 'assets/shared/fonts/FiraCode-Regular.otf', + latoRegularFont: 'assets/shared/fonts/Lato-Regular.ttf', + latoLightFont: 'assets/shared/fonts/Lato-Light.ttf', + documentPdf: 'assets/shared/TestSample Document.pdf', + imageGif: 'assets/shared/GIF Image.gif', + imageJpg: 'assets/shared/JPG Image.jpg', + imagePng: 'assets/shared/PNG Image.png', + soundMp3: 'assets/shared/MP3 Sound (1).mp3', +} as const; + +const fixtureFiles = { + mainApplicationKotlin: fs.readFileSync( + path.join(__dirname, './files/MainApplication.kt'), + ), + mainApplicationJava: fs.readFileSync( + path.join(__dirname, './files/MainApplication.java'), + ), + infoPlist: fs.readFileSync(path.join(__dirname, './files/Info.plist')), + projectPbxproj: fs.readFileSync( + path.join(__dirname, './files/project.pbxproj'), + ), + latoBoldFont: fs.readFileSync(path.join(__dirname, './files/Lato-Bold.ttf')), + latoBoldItalicFont: fs.readFileSync( + path.join(__dirname, './files/Lato-BoldItalic.ttf'), + ), + montserratRegularFont: fs.readFileSync( + path.join(__dirname, './files/Montserrat-Regular.ttf'), + ), + ralewayRegularFont: fs.readFileSync( + path.join(__dirname, './files/Raleway-Regular.ttf'), + ), + firaCodeBoldFont: fs.readFileSync( + path.join(__dirname, './files/FiraCode-Bold.otf'), + ), + firaCodeRegularFont: fs.readFileSync( + path.join(__dirname, './files/FiraCode-Regular.otf'), + ), + latoRegularFont: fs.readFileSync( + path.join(__dirname, './files/Lato-Regular.ttf'), + ), + latoLightFont: fs.readFileSync( + path.join(__dirname, './files/Lato-Light.ttf'), + ), + documentPdf: fs.readFileSync( + path.join(__dirname, './files/TestSample Document.pdf'), + ), + imageGif: fs.readFileSync(path.join(__dirname, './files/GIF Image.gif')), + imageJpg: fs.readFileSync(path.join(__dirname, './files/JPG Image.jpg')), + imagePng: fs.readFileSync(path.join(__dirname, './files/PNG Image.png')), + soundMp3: fs.readFileSync(path.join(__dirname, './files/MP3 Sound (1).mp3')), +} as const; + +const baseProject = { + // iOS project + [fixtureFilePaths.infoPlist]: fixtureFiles.infoPlist, + [fixtureFilePaths.projectPbxproj]: fixtureFiles.projectPbxproj, + + // Assets folder + [fixtureFilePaths.latoBoldFont]: fixtureFiles.latoBoldFont, + [fixtureFilePaths.latoBoldItalicFont]: fixtureFiles.latoBoldItalicFont, + [fixtureFilePaths.ralewayRegularFont]: fixtureFiles.ralewayRegularFont, + [fixtureFilePaths.firaCodeBoldFont]: fixtureFiles.firaCodeBoldFont, + [fixtureFilePaths.firaCodeRegularFont]: fixtureFiles.firaCodeRegularFont, + [fixtureFilePaths.latoRegularFont]: fixtureFiles.latoRegularFont, + [fixtureFilePaths.documentPdf]: fixtureFiles.documentPdf, + [fixtureFilePaths.imageGif]: fixtureFiles.imageGif, + [fixtureFilePaths.imageJpg]: fixtureFiles.imageJpg, + [fixtureFilePaths.imagePng]: fixtureFiles.imagePng, + [fixtureFilePaths.soundMp3]: fixtureFiles.soundMp3, +} as const; + +const baseProjectKotlin = { + ...baseProject, + + // Android project + [fixtureFilePaths.mainApplicationKotlin]: fixtureFiles.mainApplicationKotlin, +} as const; + +const baseProjectJava = { + ...baseProject, + + // Android project + [fixtureFilePaths.mainApplicationJava]: fixtureFiles.mainApplicationJava, +} as const; + +export {fixtureFilePaths, fixtureFiles, baseProjectKotlin, baseProjectJava}; diff --git a/packages/cli-link-assets/src/__tests__/__snapshots__/linkAssets.test.ts.snap b/packages/cli-link-assets/src/__tests__/__snapshots__/linkAssets.test.ts.snap new file mode 100644 index 000000000..d47ec30b5 --- /dev/null +++ b/packages/cli-link-assets/src/__tests__/__snapshots__/linkAssets.test.ts.snap @@ -0,0 +1,893 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`linkAssets should link all types of assets in a Java project for the first time 1`] = ` +"Snapshot Diff: +- First value ++ Second value + + package com.example; + ++ import com.facebook.react.common.assets.ReactFontManager; ++ + import android.app.Application; + import com.facebook.react.ReactApplication; + + public class MainApplication extends Application implements ReactApplication { + @Override + public void onCreate() { + super.onCreate(); ++ ReactFontManager.getInstance().addCustomFont(this, \\"Lato\\", R.font.lato); ++ ReactFontManager.getInstance().addCustomFont(this, \\"Fira Code\\", R.font.fira_code); + SoLoader.init(this, /* native exopackage */ false); + } + } +" +`; + +exports[`linkAssets should link all types of assets in a Java project for the first time 2`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ { ++ \\"migIndex\\": 2, ++ \\"data\\": [ ++ { ++ \\"path\\": \\"assets/shared/GIF Image.gif\\", ++ \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/JPG Image.jpg\\", ++ \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", ++ \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/PNG Image.png\\", ++ \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/TestSample Document.pdf\\", ++ \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", ++ \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", ++ \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", ++ \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" ++ }, ++ { ++ \\"path\\": \\"assets/android/fonts/Lato-Bold.ttf\\", ++ \\"sha1\\": \\"542498221d97bee5bdbccf86ee8890bf8e8005c9\\" ++ }, ++ { ++ \\"path\\": \\"assets/android/fonts/Lato-BoldItalic.ttf\\", ++ \\"sha1\\": \\"6bf491e78e16d3b9c8a55752e1bd658e15ed7f19\\" ++ } ++ ] ++ } ++" +`; + +exports[`linkAssets should link all types of assets in a Java project for the first time 3`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should link all types of assets in a Java project for the first time 4`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should link all types of assets in a Java project for the first time 5`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -30,11 +30,11 @@ + + NSAllowsLocalNetworking + + + NSLocationWhenInUseUsageDescription +- ++ + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 +@@ -45,8 +45,15 @@ + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + ++ UIAppFonts ++ ++ FiraCode-Bold.otf ++ FiraCode-Regular.otf ++ Lato-Regular.ttf ++ Raleway-Regular.ttf ++ + + +" +`; + +exports[`linkAssets should link all types of assets in a Java project for the first time 6`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ { ++ \\"migIndex\\": 2, ++ \\"data\\": [ ++ { ++ \\"path\\": \\"assets/shared/GIF Image.gif\\", ++ \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/JPG Image.jpg\\", ++ \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", ++ \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/PNG Image.png\\", ++ \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/TestSample Document.pdf\\", ++ \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", ++ \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", ++ \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", ++ \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" ++ }, ++ { ++ \\"path\\": \\"assets/ios/fonts/Raleway-Regular.ttf\\", ++ \\"sha1\\": \\"c01aaff04ead4a08b89bcb81d3d3d954345eb67f\\" ++ } ++ ] ++ } ++" +`; + +exports[`linkAssets should link all types of assets in a Kotlin project for the first time 1`] = ` +"Snapshot Diff: +- First value ++ Second value + + package com.example + ++ import com.facebook.react.common.assets.ReactFontManager ++ + import android.app.Application + import com.facebook.react.ReactApplication + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() ++ ReactFontManager.getInstance().addCustomFont(this, \\"Lato\\", R.font.lato) ++ ReactFontManager.getInstance().addCustomFont(this, \\"Fira Code\\", R.font.fira_code) + SoLoader.init(this, false) + } + } +" +`; + +exports[`linkAssets should link all types of assets in a Kotlin project for the first time 2`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ { ++ \\"migIndex\\": 2, ++ \\"data\\": [ ++ { ++ \\"path\\": \\"assets/shared/GIF Image.gif\\", ++ \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/JPG Image.jpg\\", ++ \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", ++ \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/PNG Image.png\\", ++ \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/TestSample Document.pdf\\", ++ \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", ++ \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", ++ \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", ++ \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" ++ }, ++ { ++ \\"path\\": \\"assets/android/fonts/Lato-Bold.ttf\\", ++ \\"sha1\\": \\"542498221d97bee5bdbccf86ee8890bf8e8005c9\\" ++ }, ++ { ++ \\"path\\": \\"assets/android/fonts/Lato-BoldItalic.ttf\\", ++ \\"sha1\\": \\"6bf491e78e16d3b9c8a55752e1bd658e15ed7f19\\" ++ } ++ ] ++ } ++" +`; + +exports[`linkAssets should link all types of assets in a Kotlin project for the first time 3`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should link all types of assets in a Kotlin project for the first time 4`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should link all types of assets in a Kotlin project for the first time 5`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -30,11 +30,11 @@ + + NSAllowsLocalNetworking + + + NSLocationWhenInUseUsageDescription +- ++ + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 +@@ -45,8 +45,15 @@ + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + ++ UIAppFonts ++ ++ FiraCode-Bold.otf ++ FiraCode-Regular.otf ++ Lato-Regular.ttf ++ Raleway-Regular.ttf ++ + + +" +`; + +exports[`linkAssets should link all types of assets in a Kotlin project for the first time 6`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ { ++ \\"migIndex\\": 2, ++ \\"data\\": [ ++ { ++ \\"path\\": \\"assets/shared/GIF Image.gif\\", ++ \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/JPG Image.jpg\\", ++ \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", ++ \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/PNG Image.png\\", ++ \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/TestSample Document.pdf\\", ++ \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", ++ \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", ++ \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" ++ }, ++ { ++ \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", ++ \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" ++ }, ++ { ++ \\"path\\": \\"assets/ios/fonts/Raleway-Regular.ttf\\", ++ \\"sha1\\": \\"c01aaff04ead4a08b89bcb81d3d3d954345eb67f\\" ++ } ++ ] ++ } ++" +`; + +exports[`linkAssets should link new assets in a project 1`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -28,19 +28,27 @@ + { + \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", + \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" + }, + { ++ \\"path\\": \\"assets/shared/fonts/Lato-Light.ttf\\", ++ \\"sha1\\": \\"ad0d178564445a535b15d417f5b18019923d3bab\\" ++ }, ++ { + \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", + \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" + }, + { + \\"path\\": \\"assets/android/fonts/Lato-Bold.ttf\\", + \\"sha1\\": \\"542498221d97bee5bdbccf86ee8890bf8e8005c9\\" + }, + { + \\"path\\": \\"assets/android/fonts/Lato-BoldItalic.ttf\\", + \\"sha1\\": \\"6bf491e78e16d3b9c8a55752e1bd658e15ed7f19\\" ++ }, ++ { ++ \\"path\\": \\"assets/android/fonts/Montserrat-Regular.ttf\\", ++ \\"sha1\\": \\"bb895d19b8a1fbe1c57fc89cac5da82fdc8fdef4\\" + } + ] + } +" +`; + +exports[`linkAssets should link new assets in a project 2`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -6,10 +6,11 @@ + import com.facebook.react.ReactApplication + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() ++ ReactFontManager.getInstance().addCustomFont(this, \\"Montserrat\\", R.font.montserrat) + ReactFontManager.getInstance().addCustomFont(this, \\"Lato\\", R.font.lato) + ReactFontManager.getInstance().addCustomFont(this, \\"Fira Code\\", R.font.fira_code) + SoLoader.init(this, false) + } + }" +`; + +exports[`linkAssets should link new assets in a project 3`] = ` +"Snapshot Diff: +- First value ++ Second value + + + ++ + + + + +" +`; + +exports[`linkAssets should link new assets in a project 4`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should link new assets in a project 5`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -28,10 +28,14 @@ + { + \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", + \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" + }, + { ++ \\"path\\": \\"assets/shared/fonts/Lato-Light.ttf\\", ++ \\"sha1\\": \\"ad0d178564445a535b15d417f5b18019923d3bab\\" ++ }, ++ { + \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", + \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" + }, + { + \\"path\\": \\"assets/ios/fonts/Raleway-Regular.ttf\\"," +`; + +exports[`linkAssets should link new assets in a project 6`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -51,9 +51,10 @@ + + FiraCode-Bold.otf + FiraCode-Regular.otf + Lato-Regular.ttf + Raleway-Regular.ttf ++ Lato-Light.ttf + + + +" +`; + +exports[`linkAssets should relink font assets from an Android project to use XML resources 1`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -1,7 +1,7 @@ + { +- \\"migIndex\\": 1, ++ \\"migIndex\\": 2, + \\"data\\": [ + { + \\"path\\": \\"assets/shared/GIF Image.gif\\", + \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" + }, +@@ -41,5 +41,6 @@ + \\"path\\": \\"assets/android/fonts/Lato-BoldItalic.ttf\\", + \\"sha1\\": \\"6bf491e78e16d3b9c8a55752e1bd658e15ed7f19\\" + } + ] + } ++" +`; + +exports[`linkAssets should relink font assets from an Android project to use XML resources 2`] = ` +"Snapshot Diff: +- First value ++ Second value + + package com.example + ++ import com.facebook.react.common.assets.ReactFontManager ++ + import android.app.Application + import com.facebook.react.ReactApplication + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() ++ ReactFontManager.getInstance().addCustomFont(this, \\"Lato\\", R.font.lato) ++ ReactFontManager.getInstance().addCustomFont(this, \\"Fira Code\\", R.font.fira_code) + SoLoader.init(this, false) + } + } +" +`; + +exports[`linkAssets should relink font assets from an Android project to use XML resources 3`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should relink font assets from an Android project to use XML resources 4`] = ` +"Snapshot Diff: +- First value ++ Second value + ++ ++ ++ ++ ++ ++" +`; + +exports[`linkAssets should relink font assets from an Android project to use XML resources 5`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -1,7 +1,7 @@ + { +- \\"migIndex\\": 1, ++ \\"migIndex\\": 2, + \\"data\\": [ + { + \\"path\\": \\"assets/shared/GIF Image.gif\\", + \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" + }, +@@ -37,5 +37,6 @@ + \\"path\\": \\"assets/ios/fonts/Raleway-Regular.ttf\\", + \\"sha1\\": \\"c01aaff04ead4a08b89bcb81d3d3d954345eb67f\\" + } + ] + } ++" +`; + +exports[`linkAssets should unlink all assets in a project 1`] = ` +"Snapshot Diff: +- First value ++ Second value + + { + \\"migIndex\\": 2, +- \\"data\\": [ +- { +- \\"path\\": \\"assets/shared/GIF Image.gif\\", +- \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" +- }, +- { +- \\"path\\": \\"assets/shared/JPG Image.jpg\\", +- \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" +- }, +- { +- \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", +- \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" +- }, +- { +- \\"path\\": \\"assets/shared/PNG Image.png\\", +- \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" +- }, +- { +- \\"path\\": \\"assets/shared/TestSample Document.pdf\\", +- \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", +- \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", +- \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", +- \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" +- }, +- { +- \\"path\\": \\"assets/android/fonts/Lato-Bold.ttf\\", +- \\"sha1\\": \\"542498221d97bee5bdbccf86ee8890bf8e8005c9\\" +- }, +- { +- \\"path\\": \\"assets/android/fonts/Lato-BoldItalic.ttf\\", +- \\"sha1\\": \\"6bf491e78e16d3b9c8a55752e1bd658e15ed7f19\\" +- } +- ] ++ \\"data\\": [] + } +" +`; + +exports[`linkAssets should unlink all assets in a project 2`] = ` +"Snapshot Diff: +- First value ++ Second value + + package com.example + +- import com.facebook.react.common.assets.ReactFontManager + + import android.app.Application + import com.facebook.react.ReactApplication + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() +- ReactFontManager.getInstance().addCustomFont(this, \\"Lato\\", R.font.lato) +- ReactFontManager.getInstance().addCustomFont(this, \\"Fira Code\\", R.font.fira_code) + SoLoader.init(this, false) + } + } +" +`; + +exports[`linkAssets should unlink all assets in a project 3`] = ` +"Snapshot Diff: +- First value ++ Second value + + { + \\"migIndex\\": 2, +- \\"data\\": [ +- { +- \\"path\\": \\"assets/shared/GIF Image.gif\\", +- \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" +- }, +- { +- \\"path\\": \\"assets/shared/JPG Image.jpg\\", +- \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" +- }, +- { +- \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", +- \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" +- }, +- { +- \\"path\\": \\"assets/shared/PNG Image.png\\", +- \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" +- }, +- { +- \\"path\\": \\"assets/shared/TestSample Document.pdf\\", +- \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", +- \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", +- \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", +- \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" +- }, +- { +- \\"path\\": \\"assets/ios/fonts/Raleway-Regular.ttf\\", +- \\"sha1\\": \\"c01aaff04ead4a08b89bcb81d3d3d954345eb67f\\" +- } +- ] ++ \\"data\\": [] + } +" +`; + +exports[`linkAssets should unlink all assets in a project 4`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -46,14 +46,9 @@ + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIAppFonts +- +- FiraCode-Bold.otf +- FiraCode-Regular.otf +- Lato-Regular.ttf +- Raleway-Regular.ttf +- ++ + + +" +`; + +exports[`linkAssets should unlink deleted assets in a project 1`] = ` +"Snapshot Diff: +- First value ++ Second value + + { + \\"migIndex\\": 2, + \\"data\\": [ + { +- \\"path\\": \\"assets/shared/GIF Image.gif\\", +- \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" +- }, +- { + \\"path\\": \\"assets/shared/JPG Image.jpg\\", + \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" +- }, +- { +- \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", +- \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" + }, + { + \\"path\\": \\"assets/shared/PNG Image.png\\", + \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" +- }, +- { +- \\"path\\": \\"assets/shared/TestSample Document.pdf\\", +- \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", +- \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" + }, + { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", +- \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" +- }, +- { + \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", + \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" + }, + { + \\"path\\": \\"assets/android/fonts/Lato-Bold.ttf\\", + \\"sha1\\": \\"542498221d97bee5bdbccf86ee8890bf8e8005c9\\" +- }, +- { +- \\"path\\": \\"assets/android/fonts/Lato-BoldItalic.ttf\\", +- \\"sha1\\": \\"6bf491e78e16d3b9c8a55752e1bd658e15ed7f19\\" + } + ] + } +" +`; + +exports[`linkAssets should unlink deleted assets in a project 2`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -7,10 +7,9 @@ + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + ReactFontManager.getInstance().addCustomFont(this, \\"Lato\\", R.font.lato) +- ReactFontManager.getInstance().addCustomFont(this, \\"Fira Code\\", R.font.fira_code) + SoLoader.init(this, false) + } + } +" +`; + +exports[`linkAssets should unlink deleted assets in a project 3`] = ` +"Snapshot Diff: +- First value ++ Second value + + + + +- + + +" +`; + +exports[`linkAssets should unlink deleted assets in a project 4`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -1,35 +1,15 @@ + { + \\"migIndex\\": 2, + \\"data\\": [ + { +- \\"path\\": \\"assets/shared/GIF Image.gif\\", +- \\"sha1\\": \\"da39a3ee5e6b4b0d3255bfef95601890afd80709\\" +- }, +- { + \\"path\\": \\"assets/shared/JPG Image.jpg\\", + \\"sha1\\": \\"255148944427577e1a21a5a62a1d98aa3269e9e8\\" +- }, +- { +- \\"path\\": \\"assets/shared/MP3 Sound (1).mp3\\", +- \\"sha1\\": \\"1bd4b065508235aaa400ba4e019fbfb2cb7d291c\\" + }, + { + \\"path\\": \\"assets/shared/PNG Image.png\\", + \\"sha1\\": \\"f1498c79d91acbb2291368fa1ea629ad2332a935\\" +- }, +- { +- \\"path\\": \\"assets/shared/TestSample Document.pdf\\", +- \\"sha1\\": \\"0ba2141b8996a615d7484536d7a97c27a1768407\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Bold.otf\\", +- \\"sha1\\": \\"cdb344c9982562a59831836170615e503af0db22\\" +- }, +- { +- \\"path\\": \\"assets/shared/fonts/FiraCode-Regular.otf\\", +- \\"sha1\\": \\"5115ac0f821964b0bc2938273b37be4088f3cd8e\\" + }, + { + \\"path\\": \\"assets/shared/fonts/Lato-Regular.ttf\\", + \\"sha1\\": \\"e923c72eda5e50a87e18ff5c71e9ef4b3b6455a3\\" + }," +`; + +exports[`linkAssets should unlink deleted assets in a project 5`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -47,12 +47,10 @@ + + UIViewControllerBasedStatusBarAppearance + + UIAppFonts + +- FiraCode-Bold.otf +- FiraCode-Regular.otf + Lato-Regular.ttf + Raleway-Regular.ttf + + + " +`; diff --git a/packages/cli-link-assets/src/__tests__/linkAssets.test.ts b/packages/cli-link-assets/src/__tests__/linkAssets.test.ts new file mode 100644 index 000000000..a6b6642fa --- /dev/null +++ b/packages/cli-link-assets/src/__tests__/linkAssets.test.ts @@ -0,0 +1,588 @@ +import type {Config as CLIConfig} from '@react-native-community/cli-types'; +import type FS from 'fs'; +import type Path from 'path'; +import snapshotDiff from 'snapshot-diff'; +import {PartialDeep} from 'type-fest'; +import xcode from 'xcode'; +import {cleanup, getTempDirectory, writeFiles} from '../../../../jest/helpers'; +import { + baseProjectJava, + baseProjectKotlin, + fixtureFilePaths, + fixtureFiles, +} from '../__fixtures__/projects'; +import {linkAssets} from '../linkAssets'; +import getGroup from '../tools/helpers/xcode/getGroup'; +import {ManifestFile} from '../tools/manifest'; +import '../xcode.d.ts'; + +const fs = jest.requireActual('fs'); +const path = jest.requireActual('path'); + +const DIR = getTempDirectory('temp-project'); + +const readMainApplicationKotlinFile = () => + fs.readFileSync( + path.resolve(DIR, fixtureFilePaths.mainApplicationKotlin), + 'utf8', + ); + +const readMainApplicationJavaFile = () => + fs.readFileSync( + path.resolve(DIR, fixtureFilePaths.mainApplicationJava), + 'utf8', + ); + +const readAndroidLinkAssetsManifestFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/link-assets-manifest.json'), + 'utf8', + ); + +const readInfoPlistFile = () => + fs.readFileSync(path.resolve(DIR, fixtureFilePaths.infoPlist), 'utf8'); + +const readIOSLinkAssetsManifestFile = () => + fs.readFileSync(path.resolve(DIR, 'ios/link-assets-manifest.json'), 'utf8'); + +const readLatoXMLFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/lato.xml'), + 'utf8', + ); + +const readLatoBoldFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/lato_bold.ttf'), + 'utf8', + ); + +const readLatoBoldItalicFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/lato_bolditalic.ttf'), + 'utf8', + ); + +const readLatoRegularFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/lato_regular.ttf'), + 'utf8', + ); + +const readLatoLightFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/lato_light.ttf'), + 'utf8', + ); + +const readFiraCodeXMLFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/fira_code.xml'), + 'utf8', + ); + +const readFiraCodeBoldFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/firacode_bold.otf'), + 'utf8', + ); + +const readFiraCodeRegularFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/firacode_regular.otf'), + 'utf8', + ); + +const readMontserratXMLFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/montserrat.xml'), + 'utf8', + ); + +const readMontserratRegularFontFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/font/montserrat_regular.ttf'), + 'utf8', + ); + +const readDocumentPdfFile = () => + fs.readFileSync( + path.resolve( + DIR, + 'android/app/src/main/assets/custom/TestSample Document.pdf', + ), + 'utf8', + ); + +const readImageGifFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/drawable/gif_image.gif'), + 'utf8', + ); + +const readImageJpgFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/drawable/jpg_image.jpg'), + 'utf8', + ); + +const readImagePngFile = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/drawable/png_image.png'), + 'utf8', + ); + +const readSoundMp3File = () => + fs.readFileSync( + path.resolve(DIR, 'android/app/src/main/res/raw/mp3_sound__1_.mp3'), + 'utf8', + ); + +const getIOSProjectResourcesGroup = () => { + const project = xcode + .project(path.resolve(DIR, fixtureFilePaths.projectPbxproj)) + .parseSync(); + return getGroup(project, 'Resources'); +}; + +const testBaseProjectStructure = (isKotlinProject = true) => { + const baseProject = isKotlinProject ? baseProjectKotlin : baseProjectJava; + const mainApplicationFilePath = isKotlinProject + ? fixtureFilePaths.mainApplicationKotlin + : fixtureFilePaths.mainApplicationJava; + + // Android + expect( + snapshotDiff( + baseProject[mainApplicationFilePath].toString(), + isKotlinProject + ? readMainApplicationKotlinFile() + : readMainApplicationJavaFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff('', readAndroidLinkAssetsManifestFile()), + ).toMatchSnapshot(); + + expect(snapshotDiff('', readLatoXMLFontFile())).toMatchSnapshot(); + expect(snapshotDiff('', readFiraCodeXMLFontFile())).toMatchSnapshot(); + + const linkedAssetsMap = { + [fixtureFilePaths.latoBoldFont]: readLatoBoldFontFile(), + [fixtureFilePaths.latoBoldItalicFont]: readLatoBoldItalicFontFile(), + [fixtureFilePaths.latoRegularFont]: readLatoRegularFontFile(), + [fixtureFilePaths.firaCodeBoldFont]: readFiraCodeBoldFontFile(), + [fixtureFilePaths.firaCodeRegularFont]: readFiraCodeRegularFontFile(), + [fixtureFilePaths.documentPdf]: readDocumentPdfFile(), + [fixtureFilePaths.soundMp3]: readSoundMp3File(), + [fixtureFilePaths.imageGif]: readImageGifFile(), + [fixtureFilePaths.imageJpg]: readImageJpgFile(), + [fixtureFilePaths.imagePng]: readImagePngFile(), + }; + for (const assetEntry of Object.entries(linkedAssetsMap)) { + expect(baseProjectKotlin[assetEntry[0]].toString()).toEqual(assetEntry[1]); + } + + // iOS + expect( + snapshotDiff( + baseProjectKotlin[fixtureFilePaths.infoPlist].toString(), + readInfoPlistFile(), + ), + ).toMatchSnapshot(); + expect(snapshotDiff('', readIOSLinkAssetsManifestFile())).toMatchSnapshot(); + + const resourcesGroup = getIOSProjectResourcesGroup(); + expect(resourcesGroup?.children.length).toBe(9); + [ + fixtureFilePaths.firaCodeBoldFont, + fixtureFilePaths.firaCodeRegularFont, + fixtureFilePaths.latoRegularFont, + fixtureFilePaths.ralewayRegularFont, + fixtureFilePaths.soundMp3, + fixtureFilePaths.imageGif, + fixtureFilePaths.imageJpg, + fixtureFilePaths.imagePng, + fixtureFilePaths.documentPdf, + ] + .map((filePath) => path.basename(filePath)) + .forEach((fileName) => { + expect( + resourcesGroup?.children.some((r) => r.comment === fileName), + ).toBeTruthy(); + }); +}; + +beforeEach(() => { + cleanup(DIR); + jest.resetModules(); + jest.clearAllMocks(); +}); + +afterEach(() => cleanup(DIR)); + +describe('linkAssets', () => { + const configMock: PartialDeep = { + root: DIR, + assets: ['./assets/shared'], + project: { + android: { + sourceDir: `${DIR}/android`, + appName: 'app', + assets: ['./assets/android'], + }, + ios: { + sourceDir: `${DIR}/ios`, + assets: ['./assets/ios'], + }, + }, + }; + + it('should link all types of assets in a Kotlin project for the first time', async () => { + writeFiles(DIR, baseProjectKotlin); + + await linkAssets([], configMock as CLIConfig); + + testBaseProjectStructure(true); + }); + + it('should link all types of assets in a Java project for the first time', async () => { + writeFiles(DIR, baseProjectJava); + + await linkAssets([], configMock as CLIConfig); + + testBaseProjectStructure(false); + }); + + it('should link new assets in a project', async () => { + writeFiles(DIR, baseProjectKotlin); + + await linkAssets([], configMock as CLIConfig); + + const oldAndroidLinkAssetsManifestFile = + readAndroidLinkAssetsManifestFile(); + const oldMainApplicationFile = readMainApplicationKotlinFile(); + const oldLatoXMLFontFile = readLatoXMLFontFile(); + const oldIOSLinkAssetsManifestFile = readIOSLinkAssetsManifestFile(); + const oldInfoPlistFile = readInfoPlistFile(); + + writeFiles(DIR, { + [fixtureFilePaths.latoLightFont]: fixtureFiles.latoLightFont, + [fixtureFilePaths.montserratRegularFont]: + fixtureFiles.montserratRegularFont, + }); + + await linkAssets([], configMock as CLIConfig); + + // Android + expect( + snapshotDiff( + oldAndroidLinkAssetsManifestFile, + readAndroidLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldMainApplicationFile, readMainApplicationKotlinFile()), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldLatoXMLFontFile, readLatoXMLFontFile()), + ).toMatchSnapshot(); + expect(fixtureFiles.latoLightFont.toString()).toEqual( + readLatoLightFontFile(), + ); + expect(snapshotDiff('', readMontserratXMLFontFile())).toMatchSnapshot(); + expect(fixtureFiles.montserratRegularFont.toString()).toEqual( + readMontserratRegularFontFile(), + ); + + // iOS + expect( + snapshotDiff( + oldIOSLinkAssetsManifestFile, + readIOSLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldInfoPlistFile, readInfoPlistFile()), + ).toMatchSnapshot(); + + const resourcesGroup = getIOSProjectResourcesGroup(); + expect(resourcesGroup?.children.length).toBe(10); + [ + fixtureFilePaths.firaCodeBoldFont, + fixtureFilePaths.firaCodeRegularFont, + fixtureFilePaths.latoRegularFont, + fixtureFilePaths.ralewayRegularFont, + fixtureFilePaths.soundMp3, + fixtureFilePaths.imageGif, + fixtureFilePaths.imageJpg, + fixtureFilePaths.imagePng, + fixtureFilePaths.documentPdf, + fixtureFilePaths.latoLightFont, + ] + .map((filePath) => path.basename(filePath)) + .forEach((fileName) => { + expect( + resourcesGroup?.children.some((r) => r.comment === fileName), + ).toBeTruthy(); + }); + }); + + it('should unlink deleted assets in a project', async () => { + writeFiles(DIR, baseProjectKotlin); + + await linkAssets([], configMock as CLIConfig); + + const oldAndroidLinkAssetsManifestFile = + readAndroidLinkAssetsManifestFile(); + const oldMainApplicationFile = readMainApplicationKotlinFile(); + const oldLatoXMLFontFile = readLatoXMLFontFile(); + const oldIOSLinkAssetsManifestFile = readIOSLinkAssetsManifestFile(); + const oldInfoPlistFile = readInfoPlistFile(); + + fs.rmSync(path.resolve(DIR, fixtureFilePaths.firaCodeBoldFont)); + fs.rmSync(path.resolve(DIR, fixtureFilePaths.firaCodeRegularFont)); + fs.rmSync(path.resolve(DIR, fixtureFilePaths.latoBoldItalicFont)); + fs.rmSync(path.resolve(DIR, fixtureFilePaths.documentPdf)); + fs.rmSync(path.resolve(DIR, fixtureFilePaths.imageGif)); + fs.rmSync(path.resolve(DIR, fixtureFilePaths.soundMp3)); + + await linkAssets([], configMock as CLIConfig); + + // Android + expect( + snapshotDiff( + oldAndroidLinkAssetsManifestFile, + readAndroidLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldMainApplicationFile, readMainApplicationKotlinFile()), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldLatoXMLFontFile, readLatoXMLFontFile()), + ).toMatchSnapshot(); + expect(readFiraCodeXMLFontFile).toThrow(); + expect(readFiraCodeBoldFontFile).toThrow(); + expect(readFiraCodeRegularFontFile).toThrow(); + expect(readLatoBoldItalicFontFile).toThrow(); + expect(readDocumentPdfFile).toThrow(); + expect(readImageGifFile).toThrow(); + expect(readSoundMp3File).toThrow(); + + // iOS + expect( + snapshotDiff( + oldIOSLinkAssetsManifestFile, + readIOSLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldInfoPlistFile, readInfoPlistFile()), + ).toMatchSnapshot(); + + const resourcesGroup = getIOSProjectResourcesGroup(); + expect(resourcesGroup?.children.length).toBe(4); + [ + fixtureFilePaths.latoRegularFont, + fixtureFilePaths.ralewayRegularFont, + fixtureFilePaths.imageJpg, + fixtureFilePaths.imagePng, + ] + .map((filePath) => path.basename(filePath)) + .forEach((fileName) => { + expect( + resourcesGroup?.children.some((r) => r.comment === fileName), + ).toBeTruthy(); + }); + }); + + it('should unlink all assets in a project', async () => { + writeFiles(DIR, baseProjectKotlin); + + await linkAssets([], configMock as CLIConfig); + + const oldAndroidLinkAssetsManifestFile = + readAndroidLinkAssetsManifestFile(); + const oldMainApplicationFile = readMainApplicationKotlinFile(); + const oldIOSLinkAssetsManifestFile = readIOSLinkAssetsManifestFile(); + const oldInfoPlistFile = readInfoPlistFile(); + + const sharedAssetsPath = path.resolve(DIR, 'assets/shared'); + const androidAssetsPath = path.resolve(DIR, 'assets/android'); + const iosAssetsPath = path.resolve(DIR, 'assets/ios'); + fs.readdirSync(sharedAssetsPath).forEach((file) => + fs.rmSync(path.resolve(sharedAssetsPath, file), {recursive: true}), + ); + fs.readdirSync(androidAssetsPath).forEach((file) => + fs.rmSync(path.resolve(androidAssetsPath, file), {recursive: true}), + ); + fs.readdirSync(iosAssetsPath).forEach((file) => + fs.rmSync(path.resolve(iosAssetsPath, file), {recursive: true}), + ); + + await linkAssets([], configMock as CLIConfig); + + // Android + expect( + snapshotDiff( + oldAndroidLinkAssetsManifestFile, + readAndroidLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldMainApplicationFile, readMainApplicationKotlinFile()), + ).toMatchSnapshot(); + expect( + fs.readdirSync(path.resolve(DIR, 'android/app/src/main/assets/custom')) + .length, + ).toBe(0); + expect( + fs.readdirSync(path.resolve(DIR, 'android/app/src/main/res/drawable')) + .length, + ).toBe(0); + expect( + fs.readdirSync(path.resolve(DIR, 'android/app/src/main/res/font')).length, + ).toBe(0); + expect( + fs.readdirSync(path.resolve(DIR, 'android/app/src/main/res/raw')).length, + ).toBe(0); + + // iOS + expect( + snapshotDiff( + oldIOSLinkAssetsManifestFile, + readIOSLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldInfoPlistFile, readInfoPlistFile()), + ).toMatchSnapshot(); + + const resourcesGroup = getIOSProjectResourcesGroup(); + expect(resourcesGroup?.children.length).toBe(0); + }); + + it('should relink font assets from an Android project to use XML resources', async () => { + writeFiles(DIR, baseProjectKotlin); + + await linkAssets([], configMock as CLIConfig); + + // Change link-assets-manifest.json to simulate old version + const oldAndroidLinkAssetsManifestJson = JSON.parse( + readAndroidLinkAssetsManifestFile(), + ) as ManifestFile; + oldAndroidLinkAssetsManifestJson.migIndex = 1; + const oldAndroidLinkAssetsManifestFile = JSON.stringify( + oldAndroidLinkAssetsManifestJson, + undefined, + 2, + ); + fs.writeFileSync( + path.resolve(DIR, 'android/link-assets-manifest.json'), + oldAndroidLinkAssetsManifestFile, + ); + + // Restore MainApplication.kt to original state to simulate old version + const oldMainApplicationFile = + fixtureFiles.mainApplicationKotlin.toString(); + fs.writeFileSync( + path.resolve(DIR, fixtureFilePaths.mainApplicationKotlin), + oldMainApplicationFile, + ); + + // Change link-assets-manifest.json to simulate old version + const oldIOSLinkAssetsManifestJson = JSON.parse( + readIOSLinkAssetsManifestFile(), + ) as ManifestFile; + oldIOSLinkAssetsManifestJson.migIndex = 1; + const oldIOSLinkAssetsManifestFile = JSON.stringify( + oldIOSLinkAssetsManifestJson, + undefined, + 2, + ); + fs.writeFileSync( + path.resolve(DIR, 'ios/link-assets-manifest.json'), + oldIOSLinkAssetsManifestFile, + ); + + // Remove fonts from `res/font` to simulate old version + fs.readdirSync(path.resolve(DIR, 'android/app/src/main/res/font')).forEach( + (file) => + fs.rmSync( + path.resolve( + path.resolve(DIR, 'android/app/src/main/res/font'), + file, + ), + {recursive: true}, + ), + ); + + // Add fonts to `assets/font` to simulate old version + writeFiles(DIR, { + 'android/app/src/main/assets/fonts/FireCode-Bold.otf': + fixtureFiles.firaCodeBoldFont, + 'android/app/src/main/assets/fonts/FireCode-Regular.otf': + fixtureFiles.firaCodeRegularFont, + 'android/app/src/main/assets/fonts/Lato-Bold.ttf': + fixtureFiles.latoBoldFont, + 'android/app/src/main/assets/fonts/Lato-BoldItalic.ttf': + fixtureFiles.latoBoldItalicFont, + 'android/app/src/main/assets/fonts/Lato-Regular.ttf': + fixtureFiles.latoRegularFont, + }); + + await linkAssets([], configMock as CLIConfig); + + // Android + expect( + snapshotDiff( + oldAndroidLinkAssetsManifestFile, + readAndroidLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + expect( + snapshotDiff(oldMainApplicationFile, readMainApplicationKotlinFile()), + ).toMatchSnapshot(); + + expect(snapshotDiff('', readLatoXMLFontFile())).toMatchSnapshot(); + expect(snapshotDiff('', readFiraCodeXMLFontFile())).toMatchSnapshot(); + + const linkedAssetsMap = { + [fixtureFilePaths.latoBoldFont]: readLatoBoldFontFile(), + [fixtureFilePaths.latoBoldItalicFont]: readLatoBoldItalicFontFile(), + [fixtureFilePaths.latoRegularFont]: readLatoRegularFontFile(), + [fixtureFilePaths.firaCodeBoldFont]: readFiraCodeBoldFontFile(), + [fixtureFilePaths.firaCodeRegularFont]: readFiraCodeRegularFontFile(), + [fixtureFilePaths.documentPdf]: readDocumentPdfFile(), + [fixtureFilePaths.soundMp3]: readSoundMp3File(), + [fixtureFilePaths.imageGif]: readImageGifFile(), + [fixtureFilePaths.imageJpg]: readImageJpgFile(), + [fixtureFilePaths.imagePng]: readImagePngFile(), + }; + for (const assetEntry of Object.entries(linkedAssetsMap)) { + expect(baseProjectKotlin[assetEntry[0]].toString()).toEqual( + assetEntry[1], + ); + } + + const deletedFontAssets = [ + 'android/app/src/main/res/font/FiraCode-Bold.otf', + 'android/app/src/main/res/font/FiraCode-Regular.otf', + 'android/app/src/main/res/font/Lato-Bold.ttf', + 'android/app/src/main/res/font/Lato-BoldItalic.ttf', + 'android/app/src/main/res/font/Lato-Regular.ttf', + ]; + for (const asset of deletedFontAssets) { + expect(() => fs.readFileSync(path.resolve(DIR, asset), 'utf8')).toThrow(); + } + + // iOS + expect( + snapshotDiff( + oldIOSLinkAssetsManifestFile, + readIOSLinkAssetsManifestFile(), + ), + ).toMatchSnapshot(); + }); +}); diff --git a/packages/cli-link-assets/src/fileTypes.ts b/packages/cli-link-assets/src/fileTypes.ts new file mode 100644 index 000000000..ce2ccee8c --- /dev/null +++ b/packages/cli-link-assets/src/fileTypes.ts @@ -0,0 +1,7 @@ +const fontTypes = ['otf', 'ttf'] as const; + +const imageTypes = ['png', 'jpg', 'gif'] as const; + +const audioTypes = ['mp3'] as const; + +export {fontTypes, imageTypes, audioTypes}; diff --git a/packages/cli-link-assets/src/index.ts b/packages/cli-link-assets/src/index.ts new file mode 100644 index 000000000..a040db27d --- /dev/null +++ b/packages/cli-link-assets/src/index.ts @@ -0,0 +1,3 @@ +import {default as linkAssets} from './linkAssets'; + +export const commands = {linkAssets}; diff --git a/packages/cli-link-assets/src/linkAssets.ts b/packages/cli-link-assets/src/linkAssets.ts new file mode 100644 index 000000000..4b8c64926 --- /dev/null +++ b/packages/cli-link-assets/src/linkAssets.ts @@ -0,0 +1,239 @@ +import { + findPbxprojFile, + findXcodeProject, +} from '@react-native-community/cli-platform-apple'; +import { + CLIError, + inlineString, + logger, +} from '@react-native-community/cli-tools'; +import type {Config as CLIConfig} from '@react-native-community/cli-types'; +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; +import {audioTypes, fontTypes, imageTypes} from './fileTypes'; +import cleanAndroidAssets from './tools/cleanAssets/android'; +import cleanIOSAssets from './tools/cleanAssets/ios'; +import copyAndroidAssets from './tools/copyAssets/android'; +import copyIOSAssets from './tools/copyAssets/ios'; +import linkPlatform, { + LinkOptions, + LinkOptionsPerExt, + LinkPlatformOptions, +} from './tools/linkPlatform'; +import getManifest, {AssetPathAndSHA1} from './tools/manifest'; + +function getLinkOptions( + assetType: 'font' | 'image' | 'audio' | 'custom', + androidPath: string, + androidAppName: string, +): LinkOptions { + const baseAndroidPath = [androidPath, androidAppName, 'src', 'main']; + + let shouldUseFontXMLFiles = false; + let isFontAsset = false; + let isResourceFile = false; + switch (assetType) { + case 'font': { + baseAndroidPath.push('assets', 'fonts'); + shouldUseFontXMLFiles = true; + isFontAsset = true; + break; + } + + case 'image': { + baseAndroidPath.push('res', 'drawable'); + isResourceFile = true; + break; + } + + case 'audio': { + baseAndroidPath.push('res', 'raw'); + isResourceFile = true; + break; + } + + case 'custom': { + baseAndroidPath.push('assets', 'custom'); + break; + } + } + + return { + android: { + path: path.resolve(...baseAndroidPath), + shouldUseFontXMLFiles, + isResourceFile, + }, + ios: { + isFontAsset, + }, + }; +} + +async function linkAssets(_argv: string[], ctx: CLIConfig): Promise { + let androidPath: string = ''; + let androidAssetsPath: string[] = ctx.assets; + let androidAppName: string = ''; + if (ctx.project.android) { + androidPath = ctx.project.android.sourceDir; + androidAssetsPath = androidAssetsPath.concat(ctx.project.android.assets); + androidAppName = ctx.project.android.appName; + } + + let iosPath: string = ''; + let iosAssetsPath: string[] = ctx.assets; + let iosPbxprojFilePath: string | null = null; + if (ctx.project.ios) { + iosPath = ctx.project.ios.sourceDir; + iosAssetsPath = iosAssetsPath.concat(ctx.project.ios.assets); + + const iosProjectInfo = findXcodeProject( + fs.readdirSync(iosPath, { + encoding: 'utf8', + recursive: true, + }), + ); + + if (!iosProjectInfo) { + throw new CLIError( + `Could not find Xcode project files in "${iosPath}" folder`, + ); + } + + const pbxprojPath = findPbxprojFile(iosProjectInfo); + iosPbxprojFilePath = path.join(iosPath, pbxprojPath); + } + + const rootPath = ctx.root; + + const fontLinkOptions = fontTypes.reduce( + (result: LinkOptionsPerExt, fontType) => { + result[fontType] = getLinkOptions('font', androidPath, androidAppName); + return result; + }, + {}, + ); + + const imageLinkOptions = imageTypes.reduce( + (result: LinkOptionsPerExt, imageType) => { + result[imageType] = getLinkOptions('image', androidPath, androidAppName); + return result; + }, + {}, + ); + + const audioLinkOptions = audioTypes.reduce( + (result: LinkOptionsPerExt, audioType) => { + result[audioType] = getLinkOptions('audio', androidPath, androidAppName); + return result; + }, + {}, + ); + + const linkOptionsPerExt: LinkOptionsPerExt = { + ...fontLinkOptions, + ...imageLinkOptions, + ...audioLinkOptions, + }; + + const customLinkOptions = getLinkOptions( + 'custom', + androidPath, + androidAppName, + ); + + const androidLinkPlatformOptions: LinkPlatformOptions = { + name: 'Android', + enabled: true, + platform: 'android', + rootPath, + assetsPaths: androidAssetsPath, + manifest: getManifest(androidPath, 'android'), + platformConfig: { + exists: !!ctx.project.android, + path: androidPath, + }, + cleanAssets: cleanAndroidAssets, + copyAssets: copyAndroidAssets, + linkOptionsPerExt: { + otf: linkOptionsPerExt?.otf?.android, + ttf: linkOptionsPerExt?.ttf?.android, + png: linkOptionsPerExt?.png?.android, + jpg: linkOptionsPerExt?.jpg?.android, + gif: linkOptionsPerExt?.gif?.android, + mp3: linkOptionsPerExt?.mp3?.android, + }, + otherLinkOptions: customLinkOptions.android, + }; + + const iosLinkPlatformOptions: LinkPlatformOptions = { + name: 'iOS', + enabled: true, + platform: 'ios', + rootPath, + assetsPaths: iosAssetsPath, + manifest: getManifest(iosPath, 'ios'), + platformConfig: { + exists: !!ctx.project.ios, + path: iosPath, + pbxprojFilePath: iosPbxprojFilePath!, + }, + cleanAssets: cleanIOSAssets, + copyAssets: copyIOSAssets, + linkOptionsPerExt: { + otf: linkOptionsPerExt?.otf?.ios, + ttf: linkOptionsPerExt?.ttf?.ios, + png: linkOptionsPerExt?.png?.ios, + jpg: linkOptionsPerExt?.jpg?.ios, + gif: linkOptionsPerExt?.gif?.ios, + mp3: linkOptionsPerExt?.mp3?.ios, + }, + otherLinkOptions: customLinkOptions.ios, + }; + + let previouslyAndroidLinkedAssets: AssetPathAndSHA1[] = []; + let previouslyIOSLinkedAssets: AssetPathAndSHA1[] = []; + try { + previouslyAndroidLinkedAssets = androidLinkPlatformOptions.manifest.read(); + previouslyIOSLinkedAssets = iosLinkPlatformOptions.manifest.read(); + } catch (e) {} + + if ( + androidAssetsPath.length === 0 && + iosAssetsPath.length === 0 && + previouslyAndroidLinkedAssets.length === 0 && + previouslyIOSLinkedAssets.length === 0 + ) { + logger.info( + inlineString( + `It looks like you haven't configured your assets paths in ${chalk.bold( + 'react-native.config.js', + )} file. + To can learn more about ${chalk.bold( + 'link-assets', + )} command visit: ${chalk.underline( + 'https://github.com/react-native-community/cli/blob/main/packages/cli-link-assets/README.md', + )}`, + ), + ); + return; + } + + logger.info('Linking your assets...'); + + [androidLinkPlatformOptions, iosLinkPlatformOptions] + .filter(({enabled, platformConfig}) => enabled && platformConfig.exists) + .forEach(linkPlatform); + + logger.success('Done 🎉'); +} + +export default { + func: linkAssets, + name: 'link-assets', + description: 'Links your assets to Android / iOS projects.', + options: [], +}; + +export {linkAssets}; diff --git a/packages/cli-link-assets/src/sha1File.ts b/packages/cli-link-assets/src/sha1File.ts new file mode 100644 index 000000000..6120ca235 --- /dev/null +++ b/packages/cli-link-assets/src/sha1File.ts @@ -0,0 +1,11 @@ +import crypto from 'crypto'; +import fs from 'fs'; + +function sha1File(filePath: string): string { + const hash = crypto.createHash('sha1'); + hash.update(fs.readFileSync(filePath)); + + return hash.digest('hex'); +} + +export default sha1File; diff --git a/packages/cli-link-assets/src/tools/cleanAssets/android.ts b/packages/cli-link-assets/src/tools/cleanAssets/android.ts new file mode 100644 index 000000000..f4b95abfe --- /dev/null +++ b/packages/cli-link-assets/src/tools/cleanAssets/android.ts @@ -0,0 +1,172 @@ +import {isProjectUsingKotlin} from '@react-native-community/cli-platform-android'; +import {CLIError} from '@react-native-community/cli-tools'; +import fs from 'fs-extra'; +import path from 'path'; +import { + FontFamilyMap, + FontXMLObject, + REACT_FONT_MANAGER_IMPORT, + convertToAndroidResourceName, + getAddCustomFontMethodCall, + getFontFamily, + getFontResFolderPath, + getProjectFilePath, + getXMLFontFilePath, + getXMLFontId, + readAndParseFontFile, + readAndParseFontXMLFile, + removeLineFromFile, + writeFontXMLFile, + xmlBuilder, +} from '../helpers/font/androidFontAssetHelpers'; +import {AndroidCleanAssetsOptions, CleanAssets} from './types'; + +const cleanAssetsAndroid: CleanAssets = (assetFiles, options) => { + const { + platformPath, + platformAssetsPath, + shouldUseFontXMLFiles, + isResourceFile, + } = options as AndroidCleanAssetsOptions; + const isUsingKotlin = isProjectUsingKotlin(platformPath); + + // If the assets are not fonts and are not linked with XML files, just remove them. + if (!shouldUseFontXMLFiles) { + assetFiles.forEach((file) => { + const fileName = isResourceFile + ? convertToAndroidResourceName(path.basename(file)) + : path.basename(file); + const filePath = path.join(platformAssetsPath, fileName); + try { + fs.removeSync(filePath); + } catch (e) { + throw new CLIError( + `Failed to delete "${filePath}" asset file.`, + e as Error, + ); + } + }); + + return; + } + + const fontFamilyMap: FontFamilyMap = {}; + + assetFiles.forEach((file) => { + const fontFilePath = path.join( + getFontResFolderPath(platformPath), + convertToAndroidResourceName(path.basename(file)), + ); + + const font = readAndParseFontFile(fontFilePath); + + // Build the font family's map, where each key is the font family name, + // and each value is a object containing all the font files related to that + // font family. + const fontFamily = getFontFamily( + font.tables.name.fontFamily, + font.tables.name.preferredFamily, + ); + if (!fontFamilyMap[fontFamily]) { + fontFamilyMap[fontFamily] = { + id: convertToAndroidResourceName(fontFamily), + files: [], + }; + } + + fontFamilyMap[fontFamily].files.push({ + name: convertToAndroidResourceName(path.basename(file)), + path: fontFilePath, + weight: 0, + isItalic: false, + }); + }); + + // Read MainApplication file. + const mainApplicationFilePath = getProjectFilePath( + platformPath, + 'MainApplication', + ); + let mainApplicationFileData = fs + .readFileSync(mainApplicationFilePath) + .toString(); + + Object.entries(fontFamilyMap).forEach(([fontFamilyName, fontFamilyData]) => { + const xmlFilePath = getXMLFontFilePath(platformPath, fontFamilyData.id); + + let xmlObject: FontXMLObject; + + if (fs.existsSync(xmlFilePath)) { + // XML font file already exists, so we remove the entries. + xmlObject = readAndParseFontXMLFile(xmlFilePath); + + fontFamilyData.files.forEach((file) => { + const foundEntryIndex = xmlObject['font-family'].font.findIndex( + (entry) => entry['@_app:font'] === getXMLFontId(file.name), + ); + if (foundEntryIndex !== -1) { + xmlObject['font-family'].font.splice(foundEntryIndex, 1); + } + }); + + if (xmlObject['font-family'].font.length > 0) { + // We still have some fonts declared in the XML font file. + // Write the XML font file. + const xmlData = xmlBuilder.build(xmlObject); + writeFontXMLFile(xmlFilePath, xmlData); + } else { + try { + // We remove the XML font file and method call + // because there aren't fonts declared inside it. + fs.removeSync(xmlFilePath); + } catch (e) { + throw new CLIError( + `Failed to delete "${xmlFilePath}" XML font file.`, + e as Error, + ); + } + + mainApplicationFileData = removeLineFromFile( + mainApplicationFileData, + getAddCustomFontMethodCall( + fontFamilyName, + fontFamilyData.id, + isUsingKotlin, + ), + ); + } + } + + // If there are not usages of ReactFontManager, we try to remove the import as well. + if (!mainApplicationFileData.includes('ReactFontManager.')) { + mainApplicationFileData = removeLineFromFile( + mainApplicationFileData, + `${REACT_FONT_MANAGER_IMPORT}${isUsingKotlin ? '' : ';'}`, + ); + } + + try { + // Write the modified contents to MainApplication file. + fs.writeFileSync(mainApplicationFilePath, mainApplicationFileData); + } catch (e) { + throw new CLIError( + `Failed to update "${mainApplicationFilePath}" file.`, + e as Error, + ); + } + + // Remove the font files from assets folder. + fontFamilyData.files.forEach((file) => { + try { + fs.removeSync(file.path); + } catch (e) { + throw new CLIError( + `Failed to delete "${file.path}" font file.`, + e as Error, + ); + } + }); + }); +}; + +export default cleanAssetsAndroid; diff --git a/packages/cli-link-assets/src/tools/cleanAssets/ios.ts b/packages/cli-link-assets/src/tools/cleanAssets/ios.ts new file mode 100644 index 000000000..0b2df4b92 --- /dev/null +++ b/packages/cli-link-assets/src/tools/cleanAssets/ios.ts @@ -0,0 +1,57 @@ +import {CLIError} from '@react-native-community/cli-tools'; +import fs from 'fs-extra'; +import path from 'path'; +import xcode, {PBXFile} from 'xcode'; +import createGroupWithMessage from '../helpers/xcode/createGroupWithMessage'; +import getPlist from '../helpers/xcode/getPlist'; +import writePlist from '../helpers/xcode/writePlist'; +import {CleanAssets, IOSCleanAssetsOptions} from './types'; + +const cleanAssetsIOS: CleanAssets = (assetFiles, options) => { + const {platformPath, pbxprojFilePath, isFontAsset} = + options as IOSCleanAssetsOptions; + + const project = xcode.project(pbxprojFilePath).parseSync(); + const plist = getPlist(project, platformPath); + + if (!plist) { + throw new CLIError('Failed to find PList file.'); + } + + createGroupWithMessage(project, 'Resources'); + + function removeResourceFile(files: string[]) { + return (files ?? []) + .map( + (asset) => + project.removeResourceFile(path.relative(platformPath, asset), { + target: project.getFirstTarget().uuid, + }) as PBXFile, + ) + .filter((file) => file) // xcode returns false if file is already there + .map((file) => file.basename); + } + + const removedFiles = removeResourceFile(assetFiles); + + if (isFontAsset) { + const existingFonts = (plist.UIAppFonts as string[]) ?? []; + const allFonts = existingFonts.filter( + (file) => removedFiles.indexOf(file) === -1, + ); + plist.UIAppFonts = Array.from(new Set(allFonts)); // use Set to dedupe w/existing + } + + try { + fs.writeFileSync(pbxprojFilePath, project.writeSync()); + } catch (e) { + throw new CLIError( + `Failed to update "${pbxprojFilePath}" file.`, + e as Error, + ); + } + + writePlist(project, platformPath, plist); +}; + +export default cleanAssetsIOS; diff --git a/packages/cli-link-assets/src/tools/cleanAssets/types.ts b/packages/cli-link-assets/src/tools/cleanAssets/types.ts new file mode 100644 index 000000000..b89dc3dd2 --- /dev/null +++ b/packages/cli-link-assets/src/tools/cleanAssets/types.ts @@ -0,0 +1,21 @@ +type CleanAssetsOptions = { + platformPath: string; +}; + +type AndroidCleanAssetsOptions = CleanAssetsOptions & { + platformAssetsPath: string; + shouldUseFontXMLFiles: boolean; + isResourceFile: boolean; +}; + +type IOSCleanAssetsOptions = CleanAssetsOptions & { + pbxprojFilePath: string; + isFontAsset: boolean; +}; + +type CleanAssets = ( + assetFiles: string[], + options: AndroidCleanAssetsOptions | IOSCleanAssetsOptions, +) => void; + +export {CleanAssets, AndroidCleanAssetsOptions, IOSCleanAssetsOptions}; diff --git a/packages/cli-link-assets/src/tools/copyAssets/android.ts b/packages/cli-link-assets/src/tools/copyAssets/android.ts new file mode 100644 index 000000000..17cc7496e --- /dev/null +++ b/packages/cli-link-assets/src/tools/copyAssets/android.ts @@ -0,0 +1,248 @@ +import {isProjectUsingKotlin} from '@react-native-community/cli-platform-android'; +import {CLIError} from '@react-native-community/cli-tools'; +import fs from 'fs-extra'; +import path from 'path'; +import { + FontFamilyMap, + FontXMLObject, + REACT_FONT_MANAGER_IMPORT, + addImportToFile, + buildXMLFontObject, + buildXMLFontObjectEntry, + convertToAndroidResourceName, + getAddCustomFontMethodCall, + getFontFallbackWeight, + getFontFamily, + getFontResFolderPath, + getProjectFilePath, + getXMLFontFilePath, + getXMLFontId, + insertLineInClassMethod, + readAndParseFontFile, + readAndParseFontXMLFile, + writeFontXMLFile, + xmlBuilder, +} from '../helpers/font/androidFontAssetHelpers'; +import {AndroidCopyAssetsOptions, CopyAssets} from './types'; + +const copyAssetsAndroid: CopyAssets = (assetFiles, options) => { + const { + platformPath, + platformAssetsPath, + shouldUseFontXMLFiles, + isResourceFile, + } = options as AndroidCopyAssetsOptions; + const isUsingKotlin = isProjectUsingKotlin(platformPath); + + // If the assets are not fonts and don't need to link with XML files, just copy them. + if (!shouldUseFontXMLFiles) { + assetFiles.forEach((file) => { + const fileName = isResourceFile + ? convertToAndroidResourceName(path.basename(file)) + : path.basename(file); + const filePath = path.join(platformAssetsPath, fileName); + try { + fs.copySync(file, filePath); + } catch (e) { + throw new CLIError( + `Failed to copy "${filePath}" asset file.`, + e as Error, + ); + } + }); + return; + } + + const mainApplicationFilePath = getProjectFilePath( + platformPath, + 'MainApplication', + ); + + const fontFamilyMap: FontFamilyMap = {}; + + assetFiles.forEach((file) => { + const font = readAndParseFontFile(file); + + const { + /** + * An number whose bits represent the font style. + * Must be used in conjunction with "fsSelection". + * + * Bit 1: Italic (if set to 1). + */ + macStyle, + } = font.tables.head; + + const { + /** + * An number representing the weight of the font style. + */ + usWeightClass, + + /** + * An number whose bits represent the font style. + * Must be used in conjunction with "macStyle". + * + * Bit 0: Italic (if set to 1). + */ + fsSelection, + } = font.tables.os2; + + const { + /** + * An number representing the italic angle of the font. + */ + italicAngle, + } = font.tables.post; + + /** + * Bitmask to check if font style is italic. + * + * Reference: https://learn.microsoft.com/en-us/typography/opentype/spec/os2#fsselection + */ + const fsSelectionItalicMask = 1; + + /** + * Bitmask to check if font style is italic. + * + * Reference: https://learn.microsoft.com/en-us/typography/opentype/spec/head + */ + const macStyleItalicMask = 2; + + const weight = getFontFallbackWeight(usWeightClass); + + /** + * The font is italic if both "macStyle" and "fsSelection" italic bits are set. + * If none of the bits are set, we look at the "italicAngle" as a last resource, + * which must be different from zero to be considered italic. + */ + const isItalic = Boolean( + // eslint-disable-next-line no-bitwise + (fsSelection & fsSelectionItalicMask && macStyle & macStyleItalicMask) || + italicAngle !== 0, + ); + + // Build the font family's map, where each key is the font family name, + // and each value is a object containing all the font files related to that + // font family. + const fontFamily = getFontFamily( + font.tables.name.fontFamily, + font.tables.name.preferredFamily, + ); + if (!fontFamilyMap[fontFamily]) { + fontFamilyMap[fontFamily] = { + id: convertToAndroidResourceName(fontFamily), + files: [], + }; + } + + fontFamilyMap[fontFamily].files.push({ + name: convertToAndroidResourceName(path.basename(file)), + path: file, + weight, + isItalic, + }); + }); + + Object.entries(fontFamilyMap).forEach(([fontFamilyName, fontFamilyData]) => { + const xmlFilePath = getXMLFontFilePath(platformPath, fontFamilyData.id); + + let xmlObject: FontXMLObject; + + if (fs.existsSync(xmlFilePath)) { + // XML font file already exists, so we add new entries or replace existing ones. + xmlObject = readAndParseFontXMLFile(xmlFilePath); + + fontFamilyData.files.forEach((file) => { + const xmlEntry = buildXMLFontObjectEntry(file); + + // We can't have style / weight duplicates. + const foundEntryIndex = xmlObject['font-family'].font.findIndex( + (entry) => + entry['@_app:font'] === getXMLFontId(file.name) || + (entry['@_app:fontStyle'] === xmlEntry['@_app:fontStyle'] && + entry['@_app:fontWeight'] === xmlEntry['@_app:fontWeight']), + ); + + if (foundEntryIndex !== -1) { + xmlObject['font-family'].font[foundEntryIndex] = xmlEntry; + } else { + xmlObject['font-family'].font.push(xmlEntry); + } + }); + } else { + // XML font file doesn't exist, so we create a new one. + xmlObject = buildXMLFontObject(fontFamilyData.files); + } + + // Sort the fonts by weight and style. + xmlObject['font-family'].font.sort((a, b) => { + const compareWeight = a['@_app:fontWeight'] - b['@_app:fontWeight']; + const compareStyle = a['@_app:fontStyle'].localeCompare( + b['@_app:fontStyle'], + ); + return compareWeight || compareStyle; + }); + + const xmlData = xmlBuilder.build(xmlObject); + + // Copy the font files to font folder. + fontFamilyData.files.forEach((file) => { + try { + fs.copySync( + file.path, + path.join( + getFontResFolderPath(platformPath), + path.basename(file.name), + ), + ); + } catch (e) { + throw new CLIError( + `Failed to copy "${file.path}" font file.`, + e as Error, + ); + } + }); + + // Write the XML font file. + writeFontXMLFile(xmlFilePath, xmlData); + + // Read MainApplication file. + let mainApplicationFileData = fs + .readFileSync(mainApplicationFilePath) + .toString(); + + // Add ReactFontManager's import. + mainApplicationFileData = addImportToFile( + mainApplicationFileData, + REACT_FONT_MANAGER_IMPORT, + isUsingKotlin, + ); + + // Insert add custom font's method call. + mainApplicationFileData = insertLineInClassMethod( + mainApplicationFileData, + 'MainApplication', + 'onCreate', + getAddCustomFontMethodCall( + fontFamilyName, + fontFamilyData.id, + isUsingKotlin, + ), + `super.onCreate()${isUsingKotlin ? '' : ';'}`, + isUsingKotlin, + ); + + try { + // Write the modified contents to MainApplication file. + fs.writeFileSync(mainApplicationFilePath, mainApplicationFileData); + } catch (e) { + throw new CLIError( + `Failed to update "${mainApplicationFilePath}" file.`, + e as Error, + ); + } + }); +}; + +export default copyAssetsAndroid; diff --git a/packages/cli-link-assets/src/tools/copyAssets/ios.ts b/packages/cli-link-assets/src/tools/copyAssets/ios.ts new file mode 100644 index 000000000..e0155eae8 --- /dev/null +++ b/packages/cli-link-assets/src/tools/copyAssets/ios.ts @@ -0,0 +1,59 @@ +import {CLIError} from '@react-native-community/cli-tools'; +import fs from 'fs-extra'; +import path from 'path'; +import xcode, {PBXFile} from 'xcode'; +import createGroupWithMessage from '../helpers/xcode/createGroupWithMessage'; +import getPlist from '../helpers/xcode/getPlist'; +import writePlist from '../helpers/xcode/writePlist'; +import {CopyAssets, IOSCopyAssetsOptions} from './types'; + +/** + * This function works in a similar manner to its Android version, + * except it does not copy assets but creates Xcode Group references + */ +const copyAssets: CopyAssets = (assetFiles, options) => { + const {platformPath, pbxprojFilePath, isFontAsset} = + options as IOSCopyAssetsOptions; + + const project = xcode.project(pbxprojFilePath).parseSync(); + const plist = getPlist(project, platformPath); + + if (!plist) { + throw new CLIError('Failed to find PList file.'); + } + + createGroupWithMessage(project, 'Resources'); + + function addResourceFile(assets: string[]) { + return assets + .map( + (asset) => + project.addResourceFile(path.relative(platformPath, asset), { + target: project.getFirstTarget().uuid, + }) as PBXFile, + ) + .filter((file) => file) // xcode returns false if file is already there + .map((file) => file.basename); + } + + const addedFiles = addResourceFile(assetFiles); + + if (isFontAsset) { + const existingFonts = (plist.UIAppFonts as string[]) ?? []; + const allFonts = [...existingFonts, ...addedFiles]; + plist.UIAppFonts = Array.from(new Set(allFonts)); // use Set to dedupe w/existing + } + + try { + fs.writeFileSync(pbxprojFilePath, project.writeSync()); + } catch (e) { + throw new CLIError( + `Failed to update "${pbxprojFilePath}" file.`, + e as Error, + ); + } + + writePlist(project, platformPath, plist); +}; + +export default copyAssets; diff --git a/packages/cli-link-assets/src/tools/copyAssets/types.ts b/packages/cli-link-assets/src/tools/copyAssets/types.ts new file mode 100644 index 000000000..1cce33282 --- /dev/null +++ b/packages/cli-link-assets/src/tools/copyAssets/types.ts @@ -0,0 +1,21 @@ +type CopyAssetsOptions = { + platformPath: string; +}; + +type AndroidCopyAssetsOptions = CopyAssetsOptions & { + platformAssetsPath: string; + shouldUseFontXMLFiles: boolean; + isResourceFile: boolean; +}; + +type IOSCopyAssetsOptions = CopyAssetsOptions & { + pbxprojFilePath: string; + isFontAsset: boolean; +}; + +type CopyAssets = ( + assetFiles: string[], + options: AndroidCopyAssetsOptions | IOSCopyAssetsOptions, +) => void; + +export {CopyAssets, AndroidCopyAssetsOptions, IOSCopyAssetsOptions}; diff --git a/packages/cli-link-assets/src/tools/helpers/font/androidFontAssetHelpers.ts b/packages/cli-link-assets/src/tools/helpers/font/androidFontAssetHelpers.ts new file mode 100644 index 000000000..66546c903 --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/font/androidFontAssetHelpers.ts @@ -0,0 +1,366 @@ +import {isProjectUsingKotlin} from '@react-native-community/cli-platform-android'; +import {CLIError, logger} from '@react-native-community/cli-tools'; +import {XMLBuilder, XMLParser} from 'fast-xml-parser'; +import fs from 'fs-extra'; +import {sync as globSync} from 'glob'; +import OpenType from 'opentype.js'; +import path from 'path'; + +type FontXMLEntry = { + '@_app:font': string; + '@_app:fontStyle': string; + '@_app:fontWeight': number; +}; + +type FontXMLObject = { + '?xml': { + '@_version': string; + '@_encoding': string; + }; + 'font-family': { + '@_xmlns:app': string; + font: FontXMLEntry[]; + }; +}; + +type FontFamilyFile = { + name: string; + path: string; + weight: number; + isItalic: boolean; +}; + +type FontFamilyEntry = { + id: string; + files: FontFamilyFile[]; +}; + +type FontFamilyMap = Record; + +const REACT_FONT_MANAGER_IMPORT = + 'com.facebook.react.common.assets.ReactFontManager'; + +function toArrayBuffer(buffer: Buffer) { + const arrayBuffer = new ArrayBuffer(buffer.length); + const view = new Uint8Array(arrayBuffer); + + for (let i = 0; i < buffer.length; i += 1) { + view[i] = buffer[i]; + } + + return arrayBuffer; +} + +function convertToAndroidResourceName(str: string) { + // Extract the file name (without extension) and the extension + const extension = path.extname(str); + const baseName = path.basename(str, extension); + + // Remove any leading numbers from the base name and replace invalid characters with underscores + let cleanedBaseName = baseName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(); + + // Ensure the cleaned base name does not start with a number (prepend with an underscore if it does) + if (cleanedBaseName.match(/^[0-9]/)) { + cleanedBaseName = '_' + cleanedBaseName; + } + + // Clean the extension by removing any invalid characters and convert to lowercase + // Note: path.extname includes the dot (.) in the extension, we preserve that + const cleanedExtension = extension + .replace(/[^a-zA-Z0-9_.]/g, '') + .toLowerCase(); + + // Reconstruct the file path with the cleaned base name and cleaned extension + return cleanedBaseName + cleanedExtension; +} + +function getProjectFilePath(rootPath: string, name: string) { + const isUsingKotlin = isProjectUsingKotlin(rootPath); + const ext = isUsingKotlin ? 'kt' : 'java'; + const filePath = globSync( + path.join(rootPath, `app/src/main/java/**/${name}.${ext}`), + )[0]; + return filePath; +} + +function getFontFamily( + fontFamily: OpenType.LocalizedName, + preferredFontFamily?: OpenType.LocalizedName, +) { + const availableFontFamily = preferredFontFamily || fontFamily; + return availableFontFamily.en || Object.values(availableFontFamily)[0]; +} + +/** + * Calculate a fallback weight to ensure it is multiple of 100 and between 100 and 900. + * + * Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#fallback_weights + * + * @param weight the font's weight. + * @returns a fallback weight multiple of 100, between 100 and 900, inclusive. + */ +function getFontFallbackWeight(weight: number) { + if (weight <= 500) { + return Math.max(Math.floor(weight / 100) * 100, 100); + } else { + return Math.min(Math.ceil(weight / 100) * 100, 900); + } +} + +function getFontResFolderPath(rootPath: string) { + return path.join(rootPath, 'app/src/main/res/font'); +} + +function getXMLFontId(fontFileName: string) { + return `@font/${path.basename(fontFileName, path.extname(fontFileName))}`; +} + +function buildXMLFontObjectEntry(fontFile: FontFamilyFile): FontXMLEntry { + return { + '@_app:fontStyle': fontFile.isItalic ? 'italic' : 'normal', + '@_app:fontWeight': fontFile.weight, + '@_app:font': getXMLFontId(fontFile.name), + }; +} + +function buildXMLFontObject(fontFiles: FontFamilyFile[]): FontXMLObject { + const fonts: FontXMLEntry[] = []; + fontFiles.forEach((fontFile) => { + const xmlEntry = buildXMLFontObjectEntry(fontFile); + + // We can't have style / weight duplicates. + const foundEntryIndex = fonts.findIndex( + (font) => + font['@_app:fontStyle'] === xmlEntry['@_app:fontStyle'] && + font['@_app:fontWeight'] === xmlEntry['@_app:fontWeight'], + ); + + if (foundEntryIndex === -1) { + fonts.push(xmlEntry); + } else { + fonts[foundEntryIndex] = xmlEntry; + } + }); + + return { + '?xml': { + '@_version': '1.0', + '@_encoding': 'utf-8', + }, + 'font-family': { + '@_xmlns:app': 'http://schemas.android.com/apk/res-auto', + font: fonts, + }, + }; +} + +function getAddCustomFontMethodCall( + fontName: string, + fontId: string, + isKotlin: boolean, +) { + return `ReactFontManager.getInstance().addCustomFont(this, "${fontName}", R.font.${fontId})${ + isKotlin ? '' : ';' + }`; +} + +function addImportToFile( + fileData: string, + importToAdd: string, + isKotlin: boolean, +) { + const importRegex = new RegExp( + `import\\s+${importToAdd}${isKotlin ? '' : ';'}`, + 'gm', + ); + const existingImport = importRegex.exec(fileData); + + if (existingImport) { + return fileData; + } + + const packageRegex = isKotlin ? /package\s+[\w.]+/ : /package\s+[\w.]+;/; + const packageMatch = packageRegex.exec(fileData); + + if (packageMatch) { + return fileData.replace( + packageMatch[0], + `${packageMatch[0]}\n\nimport ${importToAdd}${isKotlin ? '' : ';'}`, + ); + } + + return fileData; +} + +function insertLineInClassMethod( + fileData: string, + targetClass: string, + targetMethod: string, + codeToInsert: string, + lineToInsertAfter: string | undefined, + isKotlin: boolean, +) { + const classRegex = new RegExp( + isKotlin + ? `class\\s+${targetClass}\\s*:\\s*\\S+\\(\\)\\s*,?\\s*(\\S+\\s*)?\\{` + : `class\\s+${targetClass}(\\s+extends\\s+\\S+)?(\\s+implements\\s+\\S+)?\\s*\\{`, + 'gm', + ); + const classMatch = classRegex.exec(fileData); + + if (!classMatch) { + logger.error(`Class ${targetClass} not found.`); + return fileData; + } + + const methodRegex = new RegExp( + isKotlin + ? `override\\s+fun\\s+${targetMethod}\\s*\\(\\)` + : `(public|protected|private)\\s+(static\\s+)?\\S+\\s+${targetMethod}\\s*\\(`, + 'gm', + ); + let methodMatch = methodRegex.exec(fileData); + + while (methodMatch) { + if (methodMatch.index > classMatch.index) { + break; + } + methodMatch = methodRegex.exec(fileData); + } + + if (!methodMatch) { + logger.error(`Method ${targetMethod} not found in class ${targetClass}.`); + return fileData; + } + + const openingBraceIndex = fileData.indexOf('{', methodMatch.index); + let closingBraceIndex = -1; + let braceCount = 1; + + for (let i = openingBraceIndex + 1; i < fileData.length; i += 1) { + if (fileData[i] === '{') { + braceCount += 1; + } else if (fileData[i] === '}') { + braceCount -= 1; + } + + if (braceCount === 0) { + closingBraceIndex = i; + break; + } + } + + if (closingBraceIndex === -1) { + logger.error( + `Could not find closing brace for method ${targetMethod} in class ${targetClass}.`, + ); + return fileData; + } + + const methodBody = fileData.slice(openingBraceIndex + 1, closingBraceIndex); + + if (methodBody.includes(codeToInsert.trim())) { + return fileData; + } + + let insertPosition = closingBraceIndex; + + if (lineToInsertAfter) { + const lineIndex = methodBody.indexOf(lineToInsertAfter.trim()); + if (lineIndex !== -1) { + insertPosition = + openingBraceIndex + 1 + lineIndex + lineToInsertAfter.trim().length; + } else { + logger.error( + `Line "${lineToInsertAfter}" not found in method ${targetMethod} of class ${targetClass}.`, + ); + return fileData; + } + } + + return `${fileData.slice( + 0, + insertPosition, + )}\n ${codeToInsert}${fileData.slice(insertPosition)}`; +} + +function removeLineFromFile(fileData: string, stringToRemove: string) { + const lines = fileData.split('\n'); + const updatedLines = lines.filter((line) => !line.includes(stringToRemove)); + return updatedLines.join('\n'); +} + +function readAndParseFontFile(filePath: string): OpenType.Font { + let buffer: Buffer; + try { + buffer = fs.readFileSync(filePath); + } catch (e) { + throw new CLIError(`Failed to read "${filePath}" font file.`, e as Error); + } + + return OpenType.parse(toArrayBuffer(buffer)); +} + +function readAndParseFontXMLFile(xmlFilePath: string): FontXMLObject { + try { + return xmlParser.parse(fs.readFileSync(xmlFilePath)); + } catch (e) { + throw new CLIError( + `Failed to read "${xmlFilePath}" XML font file.`, + e as Error, + ); + } +} + +function writeFontXMLFile(xmlFilePath: string, xmlData: string): void { + try { + fs.outputFileSync(xmlFilePath, xmlData); + } catch (e) { + throw new CLIError( + `Failed to write / update "${xmlFilePath}" XML font file.`, + e as Error, + ); + } +} + +function getXMLFontFilePath( + platformPath: string, + fontFamilyId: string, +): string { + return path.join(getFontResFolderPath(platformPath), `${fontFamilyId}.xml`); +} + +const xmlParser = new XMLParser({ + ignoreAttributes: false, + isArray: (tagName) => tagName === 'font', +}); + +const xmlBuilder = new XMLBuilder({ + format: true, + ignoreAttributes: false, + suppressEmptyNode: true, +}); + +export { + FontFamilyEntry, + FontFamilyMap, + FontXMLObject, + REACT_FONT_MANAGER_IMPORT, + addImportToFile, + buildXMLFontObject, + buildXMLFontObjectEntry, + convertToAndroidResourceName, + getAddCustomFontMethodCall, + getFontFallbackWeight, + getFontFamily, + getFontResFolderPath, + getProjectFilePath, + getXMLFontFilePath, + getXMLFontId, + insertLineInClassMethod, + readAndParseFontFile, + readAndParseFontXMLFile, + removeLineFromFile, + writeFontXMLFile, + xmlBuilder, +}; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/createGroup.ts b/packages/cli-link-assets/src/tools/helpers/xcode/createGroup.ts new file mode 100644 index 000000000..5f7a38ed3 --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/createGroup.ts @@ -0,0 +1,26 @@ +import {XcodeProject} from 'xcode'; +import getGroup, {findGroup} from './getGroup'; + +/** + * Given project and path of the group, it deeply creates a given group + * making all outer groups if necessary + * + * Returns newly created group + */ +function createGroup(project: XcodeProject, path: string) { + return path.split('/').reduce((group, name) => { + if (!findGroup(group, name)) { + const uuid = project.pbxCreateGroup(name, '""'); + + group && + group.children.push({ + value: uuid, + comment: name, + }); + } + + return project.pbxGroupByName(name); + }, getGroup(project)); +} + +export default createGroup; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/createGroupWithMessage.ts b/packages/cli-link-assets/src/tools/helpers/xcode/createGroupWithMessage.ts new file mode 100644 index 000000000..1ae81765a --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/createGroupWithMessage.ts @@ -0,0 +1,27 @@ +import {logger} from '@react-native-community/cli-tools'; + +import {XcodeProject} from 'xcode'; +import createGroup from './createGroup'; +import getGroup from './getGroup'; + +/** + * Given project and path of the group, it checks if a group exists at that path, + * and deeply creates a group for that path if its does not already exist. + * + * Returns the existing or newly created group + */ +function createGroupWithMessage(project: XcodeProject, path: string) { + let group = getGroup(project, path); + + if (!group) { + group = createGroup(project, path); + + logger.info( + `Group '${path}' does not exist in your Xcode project. We have created it automatically for you.`, + ); + } + + return group; +} + +export default createGroupWithMessage; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/getBuildProperty.ts b/packages/cli-link-assets/src/tools/helpers/xcode/getBuildProperty.ts new file mode 100644 index 000000000..090c94c73 --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/getBuildProperty.ts @@ -0,0 +1,26 @@ +import {XcodeProject} from 'xcode'; + +/** + * Gets build property from the main target build section + * + * It differs from the project.getBuildProperty exposed by xcode in the way that: + * - it only checks for build property in the main target `Debug` section + * - `xcode` library iterates over all build sections and because it misses + * an early return when property is found, it will return undefined/wrong value + * when there's another build section typically after the one you want to access + * without the property defined (e.g. CocoaPods sections appended to project + * miss INFOPLIST_FILE), see: https://github.com/alunny/node-xcode/blob/master/lib/pbxProject.js#L1765 + */ +function getBuildProperty(project: XcodeProject, prop: string) { + const target = project.getFirstTarget().firstTarget; + const config = + project.pbxXCConfigurationList()[target.buildConfigurationList]; + const buildSection = + project.pbxXCBuildConfigurationSection()[ + config.buildConfigurations[0].value + ]; + + return buildSection.buildSettings[prop]; +} + +export default getBuildProperty; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/getGroup.ts b/packages/cli-link-assets/src/tools/helpers/xcode/getGroup.ts new file mode 100644 index 000000000..a56d5830a --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/getGroup.ts @@ -0,0 +1,43 @@ +import {PBXGroup, XcodeProject} from 'xcode'; + +function getFirstProject(project: XcodeProject) { + return project.getFirstProject().firstProject; +} + +function findGroup(group: PBXGroup | undefined, name: string) { + return group?.children.find((g) => g.comment === name); +} + +/** + * Returns group from .xcodeproj if one exists, null otherwise + * + * Unlike node-xcode `pbxGroupByName` - it does not return `first-matching` + * group if multiple groups with the same name exist + * + * If path is not provided, it returns top-level group + */ +function getGroup(project: XcodeProject, path?: string) { + const firstProject = getFirstProject(project); + + let group = project.getPBXGroupByKey(firstProject.mainGroup); + + if (!path) { + return group; + } + + for (var name of path.split('/')) { + var foundGroup = findGroup(group, name); + + if (foundGroup) { + group = project.getPBXGroupByKey(foundGroup.value); + } else { + group = undefined; + break; + } + } + + return group; +} + +export default getGroup; +export {findGroup}; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/getPlist.ts b/packages/cli-link-assets/src/tools/helpers/xcode/getPlist.ts new file mode 100644 index 000000000..761f91db5 --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/getPlist.ts @@ -0,0 +1,25 @@ +import fs from 'fs'; +import plistParser, {PlistObject} from 'plist'; +import {XcodeProject} from 'xcode'; +import getPlistPath from './getPlistPath'; + +type Writeable = {-readonly [P in keyof T]: T[P]}; + +/** + * Returns Info.plist located in the iOS project + * + * Returns `null` if INFOPLIST_FILE is not specified. + */ +function getPlist(project: XcodeProject, sourceDir: string) { + const plistPath = getPlistPath(project, sourceDir); + + if (!plistPath || !fs.existsSync(plistPath)) { + return null; + } + + return plistParser.parse( + fs.readFileSync(plistPath, 'utf-8'), + ) as Writeable; +} + +export default getPlist; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/getPlistPath.ts b/packages/cli-link-assets/src/tools/helpers/xcode/getPlistPath.ts new file mode 100644 index 000000000..ac0991ff9 --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/getPlistPath.ts @@ -0,0 +1,18 @@ +import path from 'path'; +import {XcodeProject} from 'xcode'; +import getBuildProperty from './getBuildProperty'; + +function getPlistPath(project: XcodeProject, sourceDir: string) { + const plistFile = getBuildProperty(project, 'INFOPLIST_FILE'); + + if (typeof plistFile !== 'string') { + return null; + } + + return path.join( + sourceDir, + plistFile.replace(/"/g, '').replace('$(SRCROOT)', ''), + ); +} + +export default getPlistPath; diff --git a/packages/cli-link-assets/src/tools/helpers/xcode/writePlist.ts b/packages/cli-link-assets/src/tools/helpers/xcode/writePlist.ts new file mode 100644 index 000000000..76d5bc8f7 --- /dev/null +++ b/packages/cli-link-assets/src/tools/helpers/xcode/writePlist.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; +import plistParser, {PlistValue} from 'plist'; +import {XcodeProject} from 'xcode'; +import getPlistPath from './getPlistPath'; + +/** + * Writes to Info.plist located in the iOS project + * + * Returns `null` if INFOPLIST_FILE is not specified or file is non-existent. + */ +function writePlist( + project: XcodeProject, + sourceDir: string, + plist: PlistValue, +) { + const plistPath = getPlistPath(project, sourceDir); + + if (!plistPath) { + return null; + } + + // We start with an offset of -1, because Xcode maintains a custom + // indentation of the plist. + // Ref: https://github.com/facebook/react-native/issues/11668 + return fs.writeFileSync( + plistPath, + `${plistParser.build(plist, {indent: '\t', offset: -1})}\n`, + ); +} + +export default writePlist; diff --git a/packages/cli-link-assets/src/tools/linkPlatform/index.ts b/packages/cli-link-assets/src/tools/linkPlatform/index.ts new file mode 100644 index 000000000..3479bb06b --- /dev/null +++ b/packages/cli-link-assets/src/tools/linkPlatform/index.ts @@ -0,0 +1,342 @@ +import {CLIError, logger} from '@react-native-community/cli-tools'; +import fs from 'fs'; +import path from 'path'; +import sha1File from '../../sha1File'; +import {CleanAssets} from '../cleanAssets/types'; +import {CopyAssets} from '../copyAssets/types'; +import {AssetPathAndSHA1, Manifest} from '../manifest'; + +type Platform = 'android' | 'ios'; + +type AndroidPlatformConfig = {}; + +type iOSPlatformConfig = { + pbxprojFilePath: string; +}; + +type PlatformConfig = (AndroidPlatformConfig | iOSPlatformConfig) & { + exists: boolean; + path: string; +}; + +type LinkOptionAndroidConfig = { + path: string; + shouldUseFontXMLFiles: boolean; + isResourceFile: boolean; +}; + +type LinkOptionIOSConfig = { + isFontAsset: boolean; +}; + +type LinkOptions = { + android: LinkOptionAndroidConfig; + ios: LinkOptionIOSConfig; +}; + +type Extension = 'otf' | 'ttf' | 'png' | 'jpg' | 'gif' | 'mp3'; + +type LinkOptionsPerExt = { + [ext in Extension]?: LinkOptions; +}; + +type LinkPlatformOptionsPerExt = { + [ext in Extension]?: LinkOptionAndroidConfig | LinkOptionIOSConfig; +}; + +type LinkPlatformOptions = { + name: string; + enabled: boolean; + platform: Platform; + rootPath: string; + assetsPaths: string[]; + manifest: Manifest; + platformConfig: PlatformConfig; + cleanAssets: CleanAssets; + copyAssets: CopyAssets; + linkOptionsPerExt: LinkPlatformOptionsPerExt; + otherLinkOptions: LinkOptionAndroidConfig | LinkOptionIOSConfig; +}; + +type FileFilter = { + name: string; + filter: (asset: AssetPathAndSHA1) => boolean; + options?: LinkOptionAndroidConfig | LinkOptionIOSConfig; +}; + +function uniqWith(arr: T[], fn: (a: T, b: T) => boolean) { + return arr.filter( + (element, index) => arr.findIndex((step) => fn(element, step)) === index, + ); +} + +function clearDuplicated(assets: AssetPathAndSHA1[]) { + return uniqWith( + assets, + (a, b) => path.parse(a.path).base === path.parse(b.path).base, + ); +} + +const filesToIgnore = ['.DS_Store', 'Thumbs.db']; +function filterFilesToIgnore(asset: AssetPathAndSHA1) { + return filesToIgnore.indexOf(path.basename(asset.path)) === -1; +} + +function getAbsolute(filePath: string, dirPath: string) { + return path.isAbsolute(filePath) ? filePath : path.resolve(dirPath, filePath); +} +function getRelative(filePath: string, dirPath: string) { + return path.isAbsolute(filePath) + ? path.relative(dirPath, filePath) + : filePath; +} + +const filterAssetByAssetsWhichNotExists = + (assets: AssetPathAndSHA1[], rootPath: string) => + (asset: AssetPathAndSHA1) => { + const relativeFilePath = getRelative(asset.path, rootPath); + + return ( + assets + .map((otherAsset) => { + return { + ...otherAsset, + path: getRelative(otherAsset.path, rootPath), + }; + }) + .findIndex((otherAsset) => { + return ( + relativeFilePath === otherAsset.path && + asset.sha1 === otherAsset.sha1 + ); + }) === -1 + ); + }; + +function linkPlatform({ + name, + platform, + rootPath, + assetsPaths, + manifest, + platformConfig, + cleanAssets, + copyAssets, + linkOptionsPerExt, + otherLinkOptions, +}: LinkPlatformOptions) { + let showAndroidRelinkingWarning = false; + + let previouslyLinkedAssets: AssetPathAndSHA1[] = []; + try { + previouslyLinkedAssets = manifest.read().map((asset) => { + return { + ...asset, + path: asset.path.split('/').join(path.sep), // Convert path to whatever system this is + }; + }); + } catch (e) { + // ok, manifest not found meaning no need to clean + } + + let assets: AssetPathAndSHA1[] = []; + + const loadAsset = (assetMightNotAbsolute: string) => { + const asset = getAbsolute(assetMightNotAbsolute, rootPath); + + try { + const stats = fs.lstatSync(asset); + + if (stats.isDirectory()) { + fs.readdirSync(asset) + .map((file) => path.resolve(asset, file)) + .forEach(loadAsset); + } else { + const sha1 = sha1File(asset); + assets = assets.concat({ + path: asset, + sha1, + }); + } + } catch (e) { + throw new CLIError( + `Could not find "${asset}" asset or folder. Please make sure the asset or folder exists.`, + ); + } + }; + + assetsPaths.forEach(loadAsset); + + assets = clearDuplicated(assets).filter(filterFilesToIgnore); + + const fileFilters = Object.keys(linkOptionsPerExt) + .map((fileExt): FileFilter => { + return { + name: fileExt, + filter: (asset) => path.extname(asset.path) === `.${fileExt}`, + options: linkOptionsPerExt[fileExt as Extension], + }; + }) + .concat({ + name: 'custom', + filter: (asset) => + Object.keys(linkOptionsPerExt).indexOf( + path.extname(asset.path).substring(1), + ) === -1, + options: otherLinkOptions, + }); + + for (const fileFilter of fileFilters) { + const assetsToUnlink = previouslyLinkedAssets + .filter(fileFilter.filter) + .filter(filterAssetByAssetsWhichNotExists(assets, rootPath)); + + const androidAssetsToRelink = previouslyLinkedAssets + .filter(fileFilter.filter) + .filter((asset) => asset.shouldRelinkAndroidFonts); + + const assetsToLink = assets + .filter(fileFilter.filter) + .filter( + filterAssetByAssetsWhichNotExists(previouslyLinkedAssets, rootPath), + ); + + const platformPath = platformConfig.path; + const androidAssetsPath = + platform === 'android' + ? (fileFilter.options as LinkOptionAndroidConfig).path + : undefined; + const shouldUseAndroidFontXMLFiles = + platform === 'android' + ? (fileFilter.options as LinkOptionAndroidConfig).shouldUseFontXMLFiles + : undefined; + const isAndroidResourceFile = + platform === 'android' + ? (fileFilter.options as LinkOptionAndroidConfig).isResourceFile + : undefined; + const iosPbxprojFilePath = + platform === 'ios' + ? (platformConfig as iOSPlatformConfig).pbxprojFilePath + : undefined; + const isIOSFontAsset = + platform === 'ios' + ? (fileFilter.options as LinkOptionIOSConfig).isFontAsset + : undefined; + + if (androidAssetsToRelink.length > 0) { + showAndroidRelinkingWarning = true; + + logger.info( + `Relinking old ${fileFilter.name} assets from ${name} project to use XML resources`, + ); + cleanAssets( + androidAssetsToRelink.map((asset) => getAbsolute(asset.path, rootPath)), + platform === 'android' + ? { + platformPath: platformPath, + platformAssetsPath: androidAssetsPath!, + shouldUseFontXMLFiles: false, + isResourceFile: isAndroidResourceFile!, + } + : { + platformPath: platformPath, + pbxprojFilePath: iosPbxprojFilePath!, + isFontAsset: isIOSFontAsset!, + }, + ); + + copyAssets( + androidAssetsToRelink + .filter( + (androidAsset) => + !assetsToUnlink.some( + (assetToUnlink) => assetToUnlink.path === androidAsset.path, + ), + ) + .map((asset) => getAbsolute(asset.path, rootPath)), + platform === 'android' + ? { + platformPath: platformPath, + platformAssetsPath: androidAssetsPath!, + shouldUseFontXMLFiles: shouldUseAndroidFontXMLFiles!, + isResourceFile: isAndroidResourceFile!, + } + : { + platformPath: platformPath, + pbxprojFilePath: iosPbxprojFilePath!, + isFontAsset: isIOSFontAsset!, + }, + ); + } + + if (assetsToUnlink.length > 0) { + logger.info( + `Cleaning previously linked ${fileFilter.name} assets from ${name} project`, + ); + cleanAssets( + assetsToUnlink + .filter( + (assetToUnlink) => + !androidAssetsToRelink.some( + (androidAsset) => androidAsset.path === assetToUnlink.path, + ), + ) + .map((asset) => getAbsolute(asset.path, rootPath)), + platform === 'android' + ? { + platformPath: platformPath, + platformAssetsPath: androidAssetsPath!, + shouldUseFontXMLFiles: shouldUseAndroidFontXMLFiles!, + isResourceFile: isAndroidResourceFile!, + } + : { + platformPath: platformPath, + pbxprojFilePath: iosPbxprojFilePath!, + isFontAsset: isIOSFontAsset!, + }, + ); + } + + if (assetsToLink.length > 0) { + logger.info(`Linking ${fileFilter.name} assets to ${name} project`); + copyAssets( + assetsToLink.map((asset) => asset.path), + platform === 'android' + ? { + platformPath: platformPath, + platformAssetsPath: androidAssetsPath!, + shouldUseFontXMLFiles: shouldUseAndroidFontXMLFiles!, + isResourceFile: isAndroidResourceFile!, + } + : { + platformPath: platformPath, + pbxprojFilePath: iosPbxprojFilePath!, + isFontAsset: isIOSFontAsset!, + }, + ); + } + } + + manifest.write( + assets.map((asset) => ({ + ...asset, + path: path.relative(rootPath, asset.path).split(path.sep).join('/'), // Convert path to POSIX just for manifest + })), + ); // Make relative + + if (showAndroidRelinkingWarning) { + logger.warn( + "The old Android font assets were relinked in order to use XML resources. Please refer to this guide to update your application's code as well: https://github.com/callstack/react-native-asset#font-assets-linking-and-usage", + ); + } +} + +export default linkPlatform; +export { + Platform, + LinkOptionAndroidConfig, + LinkOptionIOSConfig, + LinkOptions, + LinkOptionsPerExt, + LinkPlatformOptions, +}; diff --git a/packages/cli-link-assets/src/tools/manifest/index.ts b/packages/cli-link-assets/src/tools/manifest/index.ts new file mode 100644 index 000000000..9d14559b9 --- /dev/null +++ b/packages/cli-link-assets/src/tools/manifest/index.ts @@ -0,0 +1,60 @@ +import fs from 'fs-extra'; +import path from 'path'; +import {Platform} from '../linkPlatform'; +import migrations from './migrations'; + +type Manifest = { + read: () => AssetPathAndSHA1[]; + write: (data: AssetPathAndSHA1[]) => void; +}; + +type ManifestFile = { + migIndex: number; + data: AssetPathAndSHA1[]; +}; + +type AssetPathAndSHA1 = { + path: string; + sha1: string; + shouldRelinkAndroidFonts?: boolean; +}; + +const migrationsLength = migrations.length; + +function readManifest(folderPath: string): ManifestFile { + return fs.readJsonSync(path.resolve(folderPath, 'link-assets-manifest.json')); +} + +function writeManifest(folderPath: string, data: ManifestFile) { + return fs.writeJsonSync( + path.resolve(folderPath, 'link-assets-manifest.json'), + data, + { + spaces: 2, + }, + ); +} + +const getManifest = (folderPath: string, platform: Platform): Manifest => ({ + read: (): AssetPathAndSHA1[] => { + const initialData = readManifest(folderPath); + + const newManifest = migrations + .filter((_, i) => i > (initialData.migIndex || -1)) + .reduce( + (currData, mig, i) => ({ + migIndex: i, + data: mig(currData.data || currData, platform), + }), + initialData, + ); + + return newManifest.data; + }, + write: (data: AssetPathAndSHA1[]) => { + writeManifest(folderPath, {migIndex: migrationsLength - 1, data}); + }, +}); + +export default getManifest; +export {AssetPathAndSHA1, Manifest, ManifestFile}; diff --git a/packages/cli-link-assets/src/tools/manifest/migrations/index.ts b/packages/cli-link-assets/src/tools/manifest/migrations/index.ts new file mode 100644 index 000000000..524446ae8 --- /dev/null +++ b/packages/cli-link-assets/src/tools/manifest/migrations/index.ts @@ -0,0 +1,15 @@ +import {AssetPathAndSHA1} from '..'; +import {Platform} from '../../linkPlatform'; +import migration0 from './migration0'; +import migration1 from './migration1'; +import migration2 from './migration2'; + +type MigrationFn = ( + assets: string[] | AssetPathAndSHA1[], + _platform: Platform, +) => AssetPathAndSHA1[]; + +const migrations: MigrationFn[] = [migration0, migration1, migration2]; + +export default migrations; +export type {MigrationFn}; diff --git a/packages/cli-link-assets/src/tools/manifest/migrations/migration0.ts b/packages/cli-link-assets/src/tools/manifest/migrations/migration0.ts new file mode 100644 index 000000000..2bfe31742 --- /dev/null +++ b/packages/cli-link-assets/src/tools/manifest/migrations/migration0.ts @@ -0,0 +1,24 @@ +import {MigrationFn} from '.'; +import {AssetPathAndSHA1} from '..'; +import sha1File from '../../../sha1File'; +import {Platform} from '../../linkPlatform'; + +const migration0: MigrationFn = ( + assets: string[] | AssetPathAndSHA1[], + _platform: Platform, +) => { + const assetsPathsAndSha1: AssetPathAndSHA1[] = []; + + for (const path of assets as string[]) { + const sha1 = sha1File(path); + + assetsPathsAndSha1.push({ + path, + sha1, + }); + } + + return assetsPathsAndSha1; +}; + +export default migration0; diff --git a/packages/cli-link-assets/src/tools/manifest/migrations/migration1.ts b/packages/cli-link-assets/src/tools/manifest/migrations/migration1.ts new file mode 100644 index 000000000..abba51a37 --- /dev/null +++ b/packages/cli-link-assets/src/tools/manifest/migrations/migration1.ts @@ -0,0 +1,15 @@ +import {MigrationFn} from '.'; +import {AssetPathAndSHA1} from '..'; +import {Platform} from '../../linkPlatform'; + +const migration1: MigrationFn = ( + assets: string[] | AssetPathAndSHA1[], + _platform: Platform, +) => { + return (assets as AssetPathAndSHA1[]).map(({path, sha1}) => ({ + sha1, + path: `./${path}`, // Doesn't really matter which relative path, will be cleaned anyway + })); +}; + +export default migration1; diff --git a/packages/cli-link-assets/src/tools/manifest/migrations/migration2.ts b/packages/cli-link-assets/src/tools/manifest/migrations/migration2.ts new file mode 100644 index 000000000..8a7f65f3f --- /dev/null +++ b/packages/cli-link-assets/src/tools/manifest/migrations/migration2.ts @@ -0,0 +1,20 @@ +import path from 'path'; +import {AssetPathAndSHA1} from '..'; +import {fontTypes} from '../../../fileTypes'; +import {Platform} from '../../linkPlatform'; + +function migration2( + assets: string[] | AssetPathAndSHA1[], + platform: Platform, +): AssetPathAndSHA1[] { + return (assets as AssetPathAndSHA1[]).map((asset) => ({ + ...asset, + shouldRelinkAndroidFonts: + platform === 'android' && + fontTypes.includes( + path.extname(asset.path).substring(1) as (typeof fontTypes)[number], + ), + })); +} + +export default migration2; diff --git a/packages/cli-link-assets/src/xcode.d.ts b/packages/cli-link-assets/src/xcode.d.ts new file mode 100644 index 000000000..8a7d653d1 --- /dev/null +++ b/packages/cli-link-assets/src/xcode.d.ts @@ -0,0 +1,504 @@ +// Extracted from expo, packages/@expo/cli/ts-declarations/xcode/index.d.ts + +interface pbxFile { + basename: string; + lastKnownFileType?: string; + group?: string; + path?: string; + fileEncoding?: number; + defaultEncoding?: number; + sourceTree: string; + includeInIndex?: number; + explicitFileType?: unknown; + settings?: object; + uuid?: string; + fileRef: string; + target?: string; +} + +declare module 'xcode' { + /** + * UUID that is a key to each fragment of PBXProject. + */ + type UUID = string; + + /** + * if has following format `${UUID}_comment` + */ + type UUIDComment = string; + + type XCObjectType = + | 'PBXBuildFile' + | 'PBXFileReference' + | 'PBXFrameworksBuildPhase' + | 'PBXGroup' + | 'PBXNativeTarget' + | 'PBXProject' + | 'PBXResourcesBuildPhase' + | 'PBXShellScriptBuildPhase' + | 'PBXSourcesBuildPhase' + | 'PBXVariantGroup' + | 'PBXTargetDependency' + | 'XCBuildConfiguration' + | 'XCConfigurationList'; + + type PBXFile = pbxFile; + + interface PBXProject { + isa: 'PBXProject'; + attributes: { + LastUpgradeCheck: number; + TargetAttributes: Record< + UUID, + { + CreatedOnToolsVersion?: string; + TestTargetID?: UUID; + LastSwiftMigration?: number; + ProvisioningStyle?: string; + } & Record + >; + }; + buildConfigurationList: UUID; + buildConfigurationList_comment: string; + compatibilityVersion: string; + developmentRegion: string; + hasScannedForEncodings: number; + knownRegions: string[]; + mainGroup: UUID; + productRefGroup: UUID; + productRefGroup_comment: string; + projectDirPath: string; + projectRoot: string; + targets: { + value: UUID; + comment: string; + }[]; + } + + interface PBXNativeTarget { + isa: 'PBXNativeTarget'; + buildConfigurationList: UUID; + buildConfigurationList_comment: string; + buildPhases: { + value: UUID; + comment: string; + }[]; + buildRules: []; + dependencies: { + value: UUID; + comment: string; + }[]; + name: string; + productName: string; + productReference: UUID; + productReference_comment: string; + productType: string; + } + + interface PBXBuildFile { + isa: 'PBXBuildFile'; + fileRef: UUID; + // "AppDelegate.m" + fileRef_comment: string; + } + + interface PBXTargetDependency { + isa: 'PBXTargetDependency'; + target: UUID; + targetProxy: UUID; + } + + interface XCConfigurationList { + isa: 'XCConfigurationList'; + buildConfigurations: { + value: UUID; + comment: string | 'Release' | 'Debug'; + }[]; + defaultConfigurationIsVisible: number; + defaultConfigurationName: string; + } + + interface XCBuildConfiguration { + isa: 'XCBuildConfiguration'; + baseConfigurationReference: UUID; + baseConfigurationReference_comment: string; + buildSettings: Record & { + // '"$(TARGET_NAME)"', + PRODUCT_NAME?: string; + // '"io.expo.demo.$(PRODUCT_NAME:rfc1034identifier)"', + PRODUCT_BUNDLE_IDENTIFIER?: string; + PROVISIONING_PROFILE_SPECIFIER?: string; + // '"$(BUILT_PRODUCTS_DIR)/rni.app/rni"' + TEST_HOST?: any; + DEVELOPMENT_TEAM?: string; + CODE_SIGN_IDENTITY?: string; + CODE_SIGN_STYLE?: string; + // '"$(TEST_HOST)"' + BUNDLE_LOADER?: string; + GCC_PREPROCESSOR_DEFINITIONS?: unknown[]; + INFOPLIST_FILE?: string; + IPHONEOS_DEPLOYMENT_TARGET?: string; + LD_RUNPATH_SEARCH_PATHS?: string; + OTHER_LDFLAGS?: unknown[]; + ASSETCATALOG_COMPILER_APPICON_NAME?: string; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME?: string; + CLANG_ANALYZER_NONNULL?: string; + CLANG_WARN_DOCUMENTATION_COMMENTS?: string; + CLANG_WARN_INFINITE_RECURSION?: string; + CLANG_WARN_SUSPICIOUS_MOVE?: string; + DEBUG_INFORMATION_FORMAT?: string; + ENABLE_TESTABILITY?: string; + GCC_NO_COMMON_BLOCKS?: string; + // 'appletvos' + SDKROOT?: string; + TARGETED_DEVICE_FAMILY?: number | string; + // '10.0' + TVOS_DEPLOYMENT_TARGET?: string; + }; + name: string; + } + + type ProductType = + | 'com.apple.product-type.application' + | 'com.apple.product-type.app-extension' + | 'com.apple.product-type.bundle' + | 'com.apple.product-type.tool' + | 'com.apple.product-type.library.dynamic' + | 'com.apple.product-type.framework' + | 'com.apple.product-type.library.static' + | 'com.apple.product-type.bundle.unit-test' + | 'com.apple.product-type.application.watchapp' + | 'com.apple.product-type.application.watchapp2' + | 'com.apple.product-type.watchkit-extension' + | 'com.apple.product-type.watchkit2-extension'; + + export interface PBXGroup { + isa: 'PBXGroup'; + children: { + value: UUID; + comment?: string; + }[]; + name: string; + path?: string; + sourceTree: '""' | unknown; + } + + export class XcodeProject { + constructor(pbxprojPath: string); + + /** + * `.pbxproj` file path. + */ + filepath: string; + + // Ex: '$(TARGET_NAME)' + productName: string; + + hash: { + headComment: string; + project: { + archiveVersion: number; + objectVersion: number; + objects: { + [T in XCObjectType]: Record< + string, + { + isa: T; + name: string; + [key: string]: any; + } + >; + }; + rootObject: string; + rootObject_comment: string; + }; + }; + + // ------------------------------------------------------------------------ + // + // `.pbxproj` related operation - starting & ending point. + // + // ------------------------------------------------------------------------ + + /** + * First step to be executed while working with `.pbxproj` file. + */ + parse(callback?: (err: Error | null, results?: string) => void): this; + + parseSync(): this; + + /** + * @returns Content of .pbxproj file. + */ + writeSync(options?: {omitEmptyValues?: boolean}): string; + + allUuids(): UUID[]; + generateUuid(): UUID; + + addPluginFile(path: unknown, opt: unknown): unknown; + removePluginFile(path: unknown, opt: unknown): unknown; + addProductFile(targetPath: unknown, opt: unknown): unknown; + removeProductFile(path: unknown, opt: unknown): unknown; + addSourceFile(path: string, opt: unknown, group: string): unknown; + removeSourceFile(path: string, opt: unknown, group: string): unknown; + addHeaderFile(path: string, opt: unknown, group: string): unknown; + removeHeaderFile(path: string, opt: unknown, group: string): unknown; + addResourceFile(path: string, opt: unknown, group?: string): unknown; + removeResourceFile(path: string, opt: unknown, group?: string): unknown; + addFramework(fpath: string, opt: unknown): unknown; + removeFramework(fpath: unknown, opt: unknown): unknown; + addCopyfile(fpath: unknown, opt: unknown): unknown; + pbxCopyfilesBuildPhaseObj(target: unknown): unknown; + addToPbxCopyfilesBuildPhase(file: unknown): void; + removeCopyfile(fpath: unknown, opt: unknown): unknown; + removeFromPbxCopyfilesBuildPhase(file: unknown): void; + addStaticLibrary(path: unknown, opt: unknown): unknown; + /** + * Adds to `PBXBuildFile` section + */ + addToPbxBuildFileSection(file: PBXFile): void; + removeFromPbxBuildFileSection(file: unknown): void; + addPbxGroup( + filePathsArray: string[], + name: string, + path: string, + sourceTree?: string, + ): {uuid: UUID; pbxGroup: PBXGroup}; + removePbxGroup(groupName: unknown): void; + addToPbxProjectSection(target: unknown): void; + addToPbxNativeTargetSection(target: unknown): void; + addToPbxFileReferenceSection(file: any): void; + removeFromPbxFileReferenceSection(file: unknown): unknown; + addToXcVersionGroupSection(file: unknown): void; + addToPluginsPbxGroup(file: unknown): void; + removeFromPluginsPbxGroup(file: unknown): unknown; + addToResourcesPbxGroup(file: unknown): void; + removeFromResourcesPbxGroup(file: unknown): unknown; + addToFrameworksPbxGroup(file: unknown): void; + removeFromFrameworksPbxGroup(file: unknown): unknown; + addToPbxEmbedFrameworksBuildPhase(file: unknown): void; + removeFromPbxEmbedFrameworksBuildPhase(file: unknown): void; + addToProductsPbxGroup(file: unknown): void; + removeFromProductsPbxGroup(file: unknown): unknown; + addToPbxSourcesBuildPhase(file: unknown): void; + removeFromPbxSourcesBuildPhase(file: unknown): void; + /** + * Adds to PBXResourcesBuildPhase` section + * @param resourcesBuildPhaseSectionKey Because there's might more than one `Resources` build phase we need to ensure file is placed under correct one. + */ + addToPbxResourcesBuildPhase(file: PBXFile): void; + removeFromPbxResourcesBuildPhase(file: unknown): void; + addToPbxFrameworksBuildPhase(file: unknown): void; + removeFromPbxFrameworksBuildPhase(file: unknown): void; + addXCConfigurationList( + configurationObjectsArray: unknown, + defaultConfigurationName: unknown, + comment: unknown, + ): { + uuid: unknown; + xcConfigurationList: { + isa: string; + buildConfigurations: unknown[]; + defaultConfigurationIsVisible: number; + defaultConfigurationName: unknown; + }; + }; + addTargetDependency( + target: unknown, + dependencyTargets: unknown, + ): { + uuid: unknown; + target: unknown; + }; + addBuildPhase( + filePathsArray: unknown, + buildPhaseType: unknown, + comment: unknown, + target: unknown, + optionsOrFolderType: unknown, + subfolderPath: unknown, + ): { + uuid: unknown; + buildPhase: { + isa: unknown; + buildActionMask: number; + files: unknown[]; + runOnlyForDeploymentPostprocessing: number; + }; + }; + /** + * Retrieves main part describing PBXProjects that are available in `.pbxproj` file. + */ + pbxProjectSection(): Record & Record; + pbxBuildFileSection(): Record & + Record; + pbxXCBuildConfigurationSection(): Record & + Record; + pbxFileReferenceSection(): Record & + Record; + pbxNativeTargetSection(): Record & + Record; + xcVersionGroupSection(): unknown; + pbxXCConfigurationList(): Record & + Record; + pbxGroupByName(name: string): PBXGroup | undefined; + /** + * @param targetName in most cases it's the name of the application + */ + pbxTargetByName(targetName: string): PBXNativeTarget | undefined; + findTargetKey(name: string): string; + pbxItemByComment(name: string, pbxSectionName: XCObjectType): unknown; + pbxSourcesBuildPhaseObj(target: unknown): unknown; + pbxResourcesBuildPhaseObj(target: unknown): unknown; + pbxFrameworksBuildPhaseObj(target: unknown): unknown; + pbxEmbedFrameworksBuildPhaseObj(target: unknown): unknown; + buildPhase(group: unknown, target: unknown): string; + buildPhaseObject(name: string, group: unknown, target: unknown): unknown; + addBuildProperty(prop: unknown, value: unknown, buildName?: string): void; + removeBuildProperty(prop: unknown, build_name: unknown): void; + updateBuildProperty(prop: string, value: unknown, build: string): void; + updateProductName(name: string): void; + removeFromFrameworkSearchPaths(file: unknown): void; + addToFrameworkSearchPaths(file: unknown): void; + removeFromLibrarySearchPaths(file: unknown): void; + addToLibrarySearchPaths(file: unknown): void; + removeFromHeaderSearchPaths(file: unknown): void; + addToHeaderSearchPaths(file: unknown): void; + addToOtherLinkerFlags(flag: unknown): void; + removeFromOtherLinkerFlags(flag: unknown): void; + addToBuildSettings(buildSetting: unknown, value: unknown): void; + removeFromBuildSettings(buildSetting: unknown): void; + /** + * Checks whether there is a file with given `filePath` in the project. + */ + hasFile(filePath: string): PBXFile | false; + addTarget( + name: unknown, + type: unknown, + subfolder: unknown, + ): { + uuid: unknown; + pbxNativeTarget: { + isa: string; + name: string; + productName: string; + productReference: unknown; + productType: string; + buildConfigurationList: unknown; + buildPhases: unknown[]; + buildRules: unknown[]; + dependencies: unknown[]; + }; + }; + /** + * Get First PBXProject that can be found in `.pbxproj` file. + */ + getFirstProject(): {uuid: UUID; firstProject: PBXProject}; + getFirstTarget(): { + uuid: UUID; + firstTarget: PBXNativeTarget; + }; + /** + * Retrieves PBXNativeTarget by the type + */ + getTarget( + productType: ProductType, + ): {uuid: UUID; target: PBXNativeTarget} | null; + addToPbxGroupType( + file: unknown, + groupKey: unknown, + groupType: unknown, + ): void; + addToPbxVariantGroup(file: unknown, groupKey: unknown): void; + addToPbxGroup(file: PBXFile, groupKey: UUID): void; + pbxCreateGroupWithType( + name: unknown, + pathName: unknown, + groupType: unknown, + ): unknown; + pbxCreateVariantGroup(name: unknown): unknown; + pbxCreateGroup(name: string, pathName: string): UUID; + removeFromPbxGroupAndType( + file: unknown, + groupKey: unknown, + groupType: unknown, + ): void; + removeFromPbxGroup(file: unknown, groupKey: unknown): void; + removeFromPbxVariantGroup(file: unknown, groupKey: unknown): void; + getPBXGroupByKeyAndType(key: unknown, groupType: unknown): unknown; + /** + * @param groupKey UUID. + */ + getPBXGroupByKey(groupKey: string): PBXGroup | undefined; + getPBXVariantGroupByKey(key: unknown): unknown; + findPBXGroupKeyAndType(criteria: unknown, groupType: unknown): string; + /** + * @param criteria Params that should be used to locate desired PBXGroup. + */ + findPBXGroupKey(criteria: {name?: string; path?: string}): UUID | undefined; + findPBXVariantGroupKey(criteria: unknown): string; + addLocalizationVariantGroup(name: unknown): { + uuid: unknown; + fileRef: unknown; + basename: unknown; + }; + addKnownRegion(name: string): void; + removeKnownRegion(name: string): void; + hasKnownRegion(name: string): boolean; + getPBXObject(name: string): unknown; + /** + * - creates `PBXFile` + * - adds to `PBXFileReference` section + * - adds to `PBXGroup` or `PBXVariantGroup` if applicable + * @returns `null` if file is already in `pbxproj`. + */ + addFile( + path: string, + group?: string, + opt?: { + plugin?: string; + target?: string; + variantGroup?: string; + lastKnownFileType?: string; + defaultEncoding?: 4; + customFramework?: true; + explicitFileType?: number; + weak?: true; + compilerFLags?: string; + embed?: boolean; + sign?: boolean; + }, + ): PBXFile | null; + removeFile(path: unknown, group: unknown, opt: unknown): unknown; + getBuildProperty(prop: unknown, build: unknown): unknown; + getBuildConfigByName(name: unknown): object; + addDataModelDocument( + filePath: unknown, + group: unknown, + opt: unknown, + ): unknown; + addTargetAttribute(prop: unknown, value: unknown, target: unknown): void; + removeTargetAttribute(prop: unknown, target: unknown): void; + } + + export function project(projectPath: string): XcodeProject; +} + +declare module 'xcode/lib/pbxFile' { + export default class PBXFile implements pbxFile { + constructor(file: string); + basename: string; + lastKnownFileType?: string; + group?: string; + path?: string; + fileEncoding?: number; + defaultEncoding?: number; + sourceTree: string; + includeInIndex?: number; + explicitFileType?: unknown; + settings?: object; + uuid?: string; + fileRef: string; + target?: string; + } +} diff --git a/packages/cli-link-assets/tsconfig.json b/packages/cli-link-assets/tsconfig.json new file mode 100644 index 000000000..2a11c3a03 --- /dev/null +++ b/packages/cli-link-assets/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build" + }, + "references": [ + {"path": "../cli-tools"}, + {"path": "../cli-types"}, + {"path": "../cli-config"}, + {"path": "../cli-platform-apple"}, + ] +} diff --git a/packages/cli-platform-android/src/config/__tests__/__snapshots__/getProjectConfig.test.ts.snap b/packages/cli-platform-android/src/config/__tests__/__snapshots__/getProjectConfig.test.ts.snap index e122e33b5..b58cfdfd3 100644 --- a/packages/cli-platform-android/src/config/__tests__/__snapshots__/getProjectConfig.test.ts.snap +++ b/packages/cli-platform-android/src/config/__tests__/__snapshots__/getProjectConfig.test.ts.snap @@ -4,6 +4,7 @@ exports[`android::getProjectConfig returns an object with android project config Object { "appName": "", "applicationId": "com.some.example", + "assets": Array [], "dependencyConfiguration": undefined, "mainActivity": ".MainActivity", "packageName": "com.some.example", @@ -17,6 +18,7 @@ exports[`android::getProjectConfig returns an object with android project config Object { "appName": "", "applicationId": "com.some.example", + "assets": Array [], "dependencyConfiguration": undefined, "mainActivity": ".MainActivity", "packageName": "com.some.example", @@ -30,6 +32,7 @@ exports[`android::getProjectConfig returns an object with android project config Object { "appName": "app", "applicationId": "com.example", + "assets": Array [], "dependencyConfiguration": undefined, "mainActivity": ".MainActivity", "packageName": "com.some.example", diff --git a/packages/cli-platform-android/src/config/findPackageClassName.ts b/packages/cli-platform-android/src/config/findPackageClassName.ts index a500f5e97..d2c91d718 100644 --- a/packages/cli-platform-android/src/config/findPackageClassName.ts +++ b/packages/cli-platform-android/src/config/findPackageClassName.ts @@ -11,8 +11,12 @@ import glob from 'fast-glob'; import path from 'path'; import {unixifyPaths} from '@react-native-community/cli-tools'; +export function getMainActivityFiles(folder: string) { + return glob.sync('**/+(*.java|*.kt)', {cwd: unixifyPaths(folder)}); +} + export default function getPackageClassName(folder: string) { - const files = glob.sync('**/+(*.java|*.kt)', {cwd: unixifyPaths(folder)}); + const files = getMainActivityFiles(folder); const packages = files .map((filePath) => fs.readFileSync(path.join(folder, filePath), 'utf8')) diff --git a/packages/cli-platform-android/src/config/index.ts b/packages/cli-platform-android/src/config/index.ts index 239def997..f914b33a7 100644 --- a/packages/cli-platform-android/src/config/index.ts +++ b/packages/cli-platform-android/src/config/index.ts @@ -85,6 +85,7 @@ export function projectConfig( watchModeCommandParams: userConfig.watchModeCommandParams, // @todo remove for RN 0.75 unstable_reactLegacyComponentNames: undefined, + assets: userConfig.assets ?? [], }; } diff --git a/packages/cli-platform-android/src/config/isProjectUsingKotlin.ts b/packages/cli-platform-android/src/config/isProjectUsingKotlin.ts new file mode 100644 index 000000000..a0b7363a0 --- /dev/null +++ b/packages/cli-platform-android/src/config/isProjectUsingKotlin.ts @@ -0,0 +1,7 @@ +import {getMainActivityFiles} from './findPackageClassName'; + +export default function isProjectUsingKotlin(sourceDir: string): boolean { + const mainActivityFiles = getMainActivityFiles(sourceDir); + + return mainActivityFiles.some((file) => file.endsWith('.kt')); +} diff --git a/packages/cli-platform-android/src/index.ts b/packages/cli-platform-android/src/index.ts index 1b2d24215..c6efa2c0f 100644 --- a/packages/cli-platform-android/src/index.ts +++ b/packages/cli-platform-android/src/index.ts @@ -11,3 +11,4 @@ export { } from './commands/runAndroid'; export {projectConfig, dependencyConfig} from './config'; export {getAndroidProject, getPackageName} from './config/getAndroidProject'; +export {default as isProjectUsingKotlin} from './config/isProjectUsingKotlin'; diff --git a/packages/cli-platform-apple/src/config/__tests__/findPbxprojFile.test.ts b/packages/cli-platform-apple/src/config/__tests__/findPbxprojFile.test.ts new file mode 100644 index 000000000..b59a20b7a --- /dev/null +++ b/packages/cli-platform-apple/src/config/__tests__/findPbxprojFile.test.ts @@ -0,0 +1,23 @@ +import findPbxprojFile from '../findPbxprojFile'; + +describe('findPbxprojFile', () => { + it('should find project.pbxproj file', () => { + expect( + findPbxprojFile({ + path: '.', + name: 'AwesomeApp.xcodeproj', + isWorkspace: false, + }), + ).toEqual('AwesomeApp.xcodeproj/project.pbxproj'); + }); + + it('should convert .xcworkspace to .xcodeproj and find project.pbxproj file', () => { + expect( + findPbxprojFile({ + path: '.', + name: 'AwesomeApp.xcworkspace', + isWorkspace: true, + }), + ).toEqual('AwesomeApp.xcodeproj/project.pbxproj'); + }); +}); diff --git a/packages/cli-platform-apple/src/config/__tests__/findXcodeProject.test.ts b/packages/cli-platform-apple/src/config/__tests__/findXcodeProject.test.ts index b496155b0..05b955250 100644 --- a/packages/cli-platform-apple/src/config/__tests__/findXcodeProject.test.ts +++ b/packages/cli-platform-apple/src/config/__tests__/findXcodeProject.test.ts @@ -24,6 +24,7 @@ describe('findXcodeProject', () => { ]), ).toEqual({ name: 'AwesomeApp.xcodeproj', + path: '.', isWorkspace: false, }); }); @@ -42,10 +43,29 @@ describe('findXcodeProject', () => { ]), ).toEqual({ name: 'AwesomeApp.xcworkspace', + path: '.', isWorkspace: true, }); }); + it('should find *.xcodeproj file inside a folder', () => { + expect( + findXcodeProject([ + '.DS_Store', + 'AwesomeApp', + 'AwesomeApp/AwesomeApp.xcodeproj', + 'AwesomeAppTests', + 'PodFile', + 'Podfile.lock', + 'Pods', + ]), + ).toEqual({ + name: 'AwesomeApp/AwesomeApp.xcodeproj', + path: 'AwesomeApp', + isWorkspace: false, + }); + }); + it('should return null if nothing found', () => { expect( findXcodeProject([ diff --git a/packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts b/packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts index 2f24a0250..73ab1db4f 100644 --- a/packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts +++ b/packages/cli-platform-apple/src/config/__tests__/getProjectConfig.test.ts @@ -43,6 +43,7 @@ describe('ios::getProjectConfig', () => { it('returns an object with ios project configuration', () => { expect(projectConfig('/flat', {})).toMatchInlineSnapshot(` Object { + "assets": Array [], "automaticPodsInstallation": undefined, "sourceDir": "/flat/ios", "watchModeCommandParams": undefined, @@ -53,6 +54,7 @@ describe('ios::getProjectConfig', () => { it('returns correct configuration when multiple Podfile are present', () => { expect(projectConfig('/multiple', {})).toMatchInlineSnapshot(` Object { + "assets": Array [], "automaticPodsInstallation": undefined, "sourceDir": "/multiple/ios", "watchModeCommandParams": undefined, diff --git a/packages/cli-platform-apple/src/config/findPbxprojFile.ts b/packages/cli-platform-apple/src/config/findPbxprojFile.ts new file mode 100644 index 000000000..664430656 --- /dev/null +++ b/packages/cli-platform-apple/src/config/findPbxprojFile.ts @@ -0,0 +1,12 @@ +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import path from 'path'; + +function findPbxprojFile(projectInfo: IOSProjectInfo): string { + return path.join( + projectInfo.path, + projectInfo.name.replace('.xcworkspace', '.xcodeproj'), + 'project.pbxproj', + ); +} + +export default findPbxprojFile; diff --git a/packages/cli-platform-apple/src/config/findXcodeProject.ts b/packages/cli-platform-apple/src/config/findXcodeProject.ts index 9b2f9737c..48398b97a 100644 --- a/packages/cli-platform-apple/src/config/findXcodeProject.ts +++ b/packages/cli-platform-apple/src/config/findXcodeProject.ts @@ -15,16 +15,19 @@ function findXcodeProject(files: Array): IOSProjectInfo | null { for (let i = sortedFiles.length - 1; i >= 0; i--) { const fileName = files[i]; const ext = path.extname(fileName); + const projectPath = path.dirname(fileName); if (ext === '.xcworkspace') { return { name: fileName, + path: projectPath, isWorkspace: true, }; } if (ext === '.xcodeproj') { return { name: fileName, + path: projectPath, isWorkspace: false, }; } diff --git a/packages/cli-platform-apple/src/config/index.ts b/packages/cli-platform-apple/src/config/index.ts index 30df5d4ef..9b002912d 100644 --- a/packages/cli-platform-apple/src/config/index.ts +++ b/packages/cli-platform-apple/src/config/index.ts @@ -59,6 +59,7 @@ export const getProjectConfig = watchModeCommandParams: userConfig.watchModeCommandParams, xcodeProject, automaticPodsInstallation: userConfig.automaticPodsInstallation, + assets: userConfig.assets ?? [], }; }; diff --git a/packages/cli-platform-apple/src/index.ts b/packages/cli-platform-apple/src/index.ts index eafc5e68b..ec0011f04 100644 --- a/packages/cli-platform-apple/src/index.ts +++ b/packages/cli-platform-apple/src/index.ts @@ -14,3 +14,6 @@ export {default as createRun} from './commands/runCommand/createRun'; export {default as getArchitecture} from './tools/getArchitecture'; export {default as installPods} from './tools/installPods'; + +export {default as findXcodeProject} from './config/findXcodeProject'; +export {default as findPbxprojFile} from './config/findPbxprojFile'; diff --git a/packages/cli-types/src/android.ts b/packages/cli-types/src/android.ts index 4ed47154f..109a8142c 100644 --- a/packages/cli-types/src/android.ts +++ b/packages/cli-types/src/android.ts @@ -8,6 +8,7 @@ export interface AndroidProjectConfig { watchModeCommandParams?: string[]; // @todo remove for RN 0.75 unstable_reactLegacyComponentNames?: string[] | null; + assets: string[]; } export type AndroidProjectParams = { @@ -19,6 +20,7 @@ export type AndroidProjectParams = { watchModeCommandParams?: string[]; // @todo remove for RN 0.75 unstable_reactLegacyComponentNames?: string[] | null; + assets?: string[]; }; export type AndroidDependencyConfig = { diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index eac915f4c..f518e8ebf 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -122,6 +122,7 @@ export interface Config { ios: IOSPlatformConfig; [name: string]: PlatformConfig; }; + assets: string[]; commands: Command[]; // @todo this should be removed: https://github.com/react-native-community/cli/issues/1261 healthChecks: []; diff --git a/packages/cli-types/src/ios.ts b/packages/cli-types/src/ios.ts index a893d9b4d..e7c3f8964 100644 --- a/packages/cli-types/src/ios.ts +++ b/packages/cli-types/src/ios.ts @@ -7,10 +7,12 @@ export interface IOSProjectParams { sourceDir?: string; watchModeCommandParams?: string[]; automaticPodsInstallation?: boolean; + assets?: string[]; } export type IOSProjectInfo = { name: string; + path: string; isWorkspace: boolean; }; @@ -19,6 +21,7 @@ export interface IOSProjectConfig { xcodeProject: IOSProjectInfo | null; watchModeCommandParams?: string[]; automaticPodsInstallation?: boolean; + assets: string[]; } export interface IOSDependencyConfig { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 94e24f866..0a064f0b7 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -8,6 +8,7 @@ {"path": "../cli-clean"}, {"path": "../cli-config"}, {"path": "../cli-doctor"}, + {"path": "../cli-link-assets"}, {"path": "../cli-server-api"}, {"path": "../cli-types"}, {"path": "../cli-debugger-ui"}, diff --git a/yarn.lock b/yarn.lock index 7bec5a6b6..7d47dd2b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2358,6 +2358,19 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/opentype.js@^1.3.8": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@types/opentype.js/-/opentype.js-1.3.8.tgz#741be92429d1c2d64b5fa79cf692f74b49d6007f" + integrity sha512-H6qeTp03jrknklSn4bpT1/9+1xCAEIU2CnjcWPkicJy8A1SKuthanbvoHYMiv79/2W3Xn1XE4gfSJFzt2U3JSw== + +"@types/plist@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" + integrity sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA== + dependencies: + "@types/node" "*" + xmlbuilder ">=11.0.1" + "@types/prettier@^1.19.0": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" @@ -2537,6 +2550,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@xmldom/xmldom@^0.8.8": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -3178,7 +3196,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.0.2, base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -3208,6 +3226,11 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== +big-integer@1.6.x: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -3249,6 +3272,20 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bplist-creator@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bplist-creator/-/bplist-creator-0.1.0.tgz#018a2d1b587f769e379ef5519103730f8963ba1e" + integrity sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg== + dependencies: + stream-buffers "2.2.x" + +bplist-parser@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.3.1.tgz#e1c90b2ca2a9f9474cc72f6862bbf3fee8341fd1" + integrity sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA== + dependencies: + big-integer "1.6.x" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5647,6 +5684,13 @@ fast-xml-parser@^4.2.4: dependencies: strnum "^1.0.5" +fast-xml-parser@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz#761e641260706d6e13251c4ef8e3f5694d4b0d79" + integrity sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg== + dependencies: + strnum "^1.0.5" + fastparse@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -9535,6 +9579,14 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +opentype.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.4.tgz#1c0e72e46288473cc4a4c6a2dc60fd7fe6020d77" + integrity sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw== + dependencies: + string.prototype.codepointat "^0.2.1" + tiny-inflate "^1.0.3" + opn@^5.1.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -10044,6 +10096,15 @@ pkg-up@^2.0.0: dependencies: find-up "^2.1.0" +plist@^3.0.5, plist@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" + integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + pn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" @@ -11468,6 +11529,15 @@ sigstore@^1.3.0, sigstore@^1.4.0: "@sigstore/tuf" "^1.0.3" make-fetch-happen "^11.0.1" +simple-plist@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.3.1.tgz#16e1d8f62c6c9b691b8383127663d834112fb017" + integrity sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw== + dependencies: + bplist-creator "0.1.0" + bplist-parser "0.3.1" + plist "^3.0.5" + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -11779,6 +11849,11 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" +stream-buffers@2.2.x: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" + integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg== + stream-http@^2.7.2: version "2.8.3" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" @@ -11844,6 +11919,11 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string.prototype.codepointat@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" + integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== + string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" @@ -12214,7 +12294,7 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-inflate@^1.0.0: +tiny-inflate@^1.0.0, tiny-inflate@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== @@ -12452,6 +12532,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.10.2: + version "4.10.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.10.2.tgz#3abdb144d93c5750432aac0d73d3e85fcab45738" + integrity sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw== + typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" @@ -12749,6 +12834,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + uuid@^8.3.0: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -13138,11 +13228,24 @@ ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +xcode@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c" + integrity sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA== + dependencies: + simple-plist "^1.1.0" + uuid "^7.0.3" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xmlbuilder@>=11.0.1, xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xmlchars@^2.1.1, xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"