├── .prettierrc.json ├── .gitignore ├── .npmignore ├── .prettierignore ├── app.plugin.js ├── .vscode └── settings.json ├── .husky └── pre-commit ├── .eslintrc.js ├── plugin ├── tsconfig.json └── src │ └── index.ts ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | plugin/build 3 | *.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | babel.config.js 2 | plugin/src 3 | *.tgz -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage -------------------------------------------------------------------------------- /app.plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./plugin/build"); 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-module-scripts 2 | module.exports = require('expo-module-scripts/eslintrc.base.js'); 3 | -------------------------------------------------------------------------------- /plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo-module-scripts/tsconfig.plugin", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "rootDir": "src" 6 | }, 7 | "include": ["./src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo-module-scripts/tsconfig.base", 3 | "compilerOptions": { 4 | "outDir": "./plugin/build", 5 | "module": "CommonJS" 6 | }, 7 | "include": ["./plugin/src"], 8 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Armaan A 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-xml-font", 3 | "version": "3.0.2", 4 | "description": "Use Android XML Fonts easily with this Expo plugin", 5 | "main": "app.plugin.js", 6 | "repository": "git@github.com:Armster15/expo-xml-font.git", 7 | "author": "Armaan A", 8 | "keywords": [ 9 | "android", 10 | "font", 11 | "fonts", 12 | "react-native", 13 | "expo" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/Armster15/expo-xml-font#readme", 17 | "bugs": { 18 | "url": "https://github.com/Armster15/expo-xml-font/issues" 19 | }, 20 | "private": false, 21 | "publishConfig": { 22 | "registry": "https://registry.npmjs.org/" 23 | }, 24 | "files": [ 25 | "app.plugin.js", 26 | "plugin/build" 27 | ], 28 | "scripts": { 29 | "build": "expo-module build", 30 | "clean": "expo-module clean", 31 | "test": "expo-module test", 32 | "prepare": "husky install", 33 | "prepublishOnly": "expo-module prepublishOnly", 34 | "expo-module": "expo-module" 35 | }, 36 | "dependencies": { 37 | "fs-extra": "^11.1.0" 38 | }, 39 | "devDependencies": { 40 | "@types/fs-extra": "^11.0.1", 41 | "expo": "^48.0.0", 42 | "expo-module-scripts": "^3.0.4", 43 | "husky": "^8.0.3", 44 | "lint-staged": "^13.1.0", 45 | "prettier": "2.8.3" 46 | }, 47 | "peerDependencies": { 48 | "@expo/config-plugins": ">=6.0.0" 49 | }, 50 | "lint-staged": { 51 | "**/*": "prettier --write --ignore-unknown" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # expo-xml-font 2 | 3 | Use Android XML Fonts easily with this Expo plugin. 4 | 5 | This plugin builds off an awesome guide available at https://github.com/jsamr/react-native-font-demo on how to use XML Fonts in React Native but provides an easy to use API so you don't have to manually fiddle with the native files. 6 | 7 | ## Installation 8 | 9 | > [!NOTE] 10 | > v3 is for Expo 50<= while v2 is for Expo 49>= 11 | 12 | 1. Install the plugin 13 | 14 | ```bash 15 | # using yarn 16 | yarn add expo-xml-font 17 | 18 | # using npm 19 | npm install expo-xml-font 20 | ``` 21 | 22 | 2. Add the plugin to the `plugins` section of your `app.json` or `app.config.js` file: 23 | 24 | ```json 25 | { 26 | "plugins": ["expo-xml-font"] 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | This example will use Inter as an example, but you can use any font you like. 33 | 34 | 1. Get your font and all of its variants 35 | 36 | ``` 37 | Inter-Thin.ttf (100) 38 | Inter-ExtraLight.ttf (200) 39 | Inter-Light.ttf (300) 40 | Inter-Regular.ttf (400) 41 | Inter-Medium.ttf (500) 42 | Inter-Semibold.ttf (600) 43 | Inter-Bold.ttf (700) 44 | Inter-Extrabold.ttf (800) 45 | Inter-Black.ttf (900) 46 | ``` 47 | 48 | 2. Change all the dashes ("-"s) to underscores ("\_") and all the uppercase letters to lowercase letters. This is required to be compatible with Android's asset names restrictions 49 | 50 | Allowed characters: lowercase letters (a-z), numbers (0-9), underscores (_) 51 | 52 | ``` 53 | inter_thin.ttf (100) 54 | inter_extraLight.ttf (200) 55 | inter_light.ttf (300) 56 | inter_regular.ttf (400) 57 | inter_medium.ttf (500) 58 | inter_semibold.ttf (600) 59 | inter_bold.ttf (700) 60 | inter_extrabold.ttf (800) 61 | inter_black.ttf (900) 62 | ``` 63 | 64 | 3. Copy these files to a folder where your project is. You could put the font files in an `assets/fonts` folder for example. 65 | 66 | 4. Configure `expo-xml-font` in your `app.json` or `app.config.js`: 67 | 68 | ```json 69 | { 70 | "plugins": [ 71 | [ 72 | "expo-xml-font", 73 | [ 74 | { 75 | "name": "Inter", 76 | "folder": "assets/fonts", 77 | "variants": [ 78 | { "fontFile": "inter_thin", "fontWeight": 100 }, 79 | { "fontFile": "inter_extralight", "fontWeight": 200 }, 80 | { "fontFile": "inter_light", "fontWeight": 300 }, 81 | { "fontFile": "inter_regular", "fontWeight": 400 }, 82 | { "fontFile": "inter_medium", "fontWeight": 500 }, 83 | { "fontFile": "inter_semibold", "fontWeight": 600 }, 84 | { "fontFile": "inter_bold", "fontWeight": 700 }, 85 | { "fontFile": "inter_extrabold", "fontWeight": 800 }, 86 | { "fontFile": "inter_black", "fontWeight": 900 } 87 | ] 88 | } 89 | ] 90 | ] 91 | ] 92 | } 93 | ``` 94 | 95 | 5. Run `expo prebuild` to generate the XML fonts. You can now use them in your code! 96 | 97 | ## Configuration 98 | 99 | When using `expo-xml-font`, you need to pass in an object providing all the details of the font. 100 | 101 | ```ts 102 | type Options = { 103 | /** 104 | * Name of font 105 | * @example "Inter" 106 | */ 107 | name: string; 108 | /** 109 | * Path of folder which contains font files. It's alright if the path is relative. 110 | * @example "assets/fonts" 111 | */ 112 | folder: string; 113 | variants: { 114 | /** 115 | * Font file for font. Do NOT include the extension. 116 | * @example "inter_regular.ttf" -> "inter_regular" 117 | */ 118 | fontFile: string; 119 | /** The font weight of the provided font file */ 120 | fontWeight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; 121 | /** 122 | * Specifies whether or not the font file is italic 123 | * @default false 124 | */ 125 | italic?: boolean; 126 | }[]; 127 | }[]; 128 | ``` 129 | 130 | Since this plugin takes in an array of fonts, you can specify as many font families as you want to. 131 | -------------------------------------------------------------------------------- /plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { withMainApplication } from "@expo/config-plugins"; 2 | import { mergeContents } from "@expo/config-plugins/build/utils/generateCode"; 3 | import fs from "fs-extra"; 4 | import path from "node:path"; 5 | import type { ExpoConfig } from "expo/config"; 6 | 7 | type WithXMLFontOptions = { 8 | /** 9 | * Name of font 10 | * @example "Inter" 11 | */ 12 | name: string; 13 | /** 14 | * Path of folder which contains font files. It's alright if the path is relative. 15 | * @example "assets/fonts" 16 | */ 17 | folder: string; 18 | variants: { 19 | /** 20 | * Font file for font. Do NOT include the extension. 21 | * @example "inter_regular.ttf" -> "inter_regular" 22 | */ 23 | fontFile: string; 24 | /** The font weight of the provided font file */ 25 | fontWeight: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; 26 | /** 27 | * Specifies whether or not the font file is italic 28 | * @default false 29 | */ 30 | italic?: boolean; 31 | }[]; 32 | }[]; 33 | 34 | /** 35 | * Expo plugin that creates an Android XML font 36 | */ 37 | const withAndroidXMLFont = (config: ExpoConfig, fonts: WithXMLFontOptions) => { 38 | return withMainApplication(config, async (config) => { 39 | // 1. Modify MainApplication.java file 40 | let mainApplication = config.modResults.contents; 41 | 42 | // Adds "import com.facebook.react.views.text.ReactFontManager" line 43 | mainApplication = mergeContents({ 44 | src: mainApplication, 45 | anchor: "import com.facebook.react.ReactPackage", 46 | newSrc: "import com.facebook.react.views.text.ReactFontManager", 47 | comment: "//", 48 | offset: 1, 49 | tag: "expo-xml-font:import-rn-font-manager-line", 50 | }).contents; 51 | 52 | // Adds custom font lines 53 | const addCustomFontLine = fonts 54 | .map( 55 | ({ name }) => 56 | `ReactFontManager.getInstance().addCustomFont(this, "${name}", R.font.${formatName(name)})` 57 | ) 58 | .join("\n"); 59 | 60 | mainApplication = mergeContents({ 61 | src: mainApplication, 62 | anchor: /^\s*super\.onCreate\(\)\s*$/, // "super.onCreate()" 63 | newSrc: addCustomFontLine, 64 | comment: "//", 65 | offset: 1, 66 | tag: "expo-xml-font:add-custom-font-line", 67 | }).contents; 68 | 69 | config.modResults.contents = mainApplication; 70 | 71 | // 2. Copy fonts to respective Android folders 72 | for (const { name, folder, variants } of fonts) { 73 | await fs.copy(folder, "android/app/src/main/res/font"); 74 | 75 | // Validates that none of the files have a "-" or uppercase letters 76 | for (const file of await fs.readdir(folder)) { 77 | if (file.includes("-") || file.toLowerCase() !== file) { 78 | throw new Error( 79 | `Font files must not have dashes ("-") and must be all lowercase.` 80 | ); 81 | } 82 | } 83 | 84 | // 3. Create XML File 85 | let xml = 86 | `\n` + 87 | `\n`; 88 | 89 | for (const { fontFile, fontWeight, italic } of variants) { 90 | xml += ` \n`; 93 | } 94 | 95 | xml += ``; 96 | 97 | await fs.writeFile( 98 | path.join("android/app/src/main/res/font", `${formatName(name)}.xml`), 99 | xml 100 | ); 101 | } 102 | 103 | return config; 104 | }); 105 | }; 106 | 107 | // Turns a string into an Android friendly file-based resource name 108 | // File-based resource names can contain only lowercase a-z, 0-9, or underscore 109 | // Ex: "Plus Jakarta Sans" -> "plusjakartasans" 110 | function formatName(name: string) { 111 | return name.toLowerCase().replace(/[^a-z0-9_]/g, '') 112 | } 113 | 114 | export default withAndroidXMLFont; 115 | --------------------------------------------------------------------------------