expo-font 13.2.1 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/android/build.gradle +2 -2
  3. package/expo-module.config.json +1 -1
  4. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.aar +0 -0
  5. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.aar.md5 +1 -0
  6. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.aar.sha1 +1 -0
  7. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.aar.sha256 +1 -0
  8. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.aar.sha512 +1 -0
  9. package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1.module → 13.3.0/expo.modules.font-13.3.0.module} +17 -17
  10. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.module.md5 +1 -0
  11. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.module.sha1 +1 -0
  12. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.module.sha256 +1 -0
  13. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.module.sha512 +1 -0
  14. package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1.pom → 13.3.0/expo.modules.font-13.3.0.pom} +1 -1
  15. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.pom.md5 +1 -0
  16. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.pom.sha1 +1 -0
  17. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.pom.sha256 +1 -0
  18. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.3.0/expo.modules.font-13.3.0.pom.sha512 +1 -0
  19. package/local-maven-repo/host/exp/exponent/expo.modules.font/maven-metadata.xml +4 -4
  20. package/local-maven-repo/host/exp/exponent/expo.modules.font/maven-metadata.xml.md5 +1 -1
  21. package/local-maven-repo/host/exp/exponent/expo.modules.font/maven-metadata.xml.sha1 +1 -1
  22. package/local-maven-repo/host/exp/exponent/expo.modules.font/maven-metadata.xml.sha256 +1 -1
  23. package/local-maven-repo/host/exp/exponent/expo.modules.font/maven-metadata.xml.sha512 +1 -1
  24. package/package.json +5 -3
  25. package/plugin/build/utils.d.ts +1 -0
  26. package/plugin/build/utils.js +12 -0
  27. package/plugin/build/withFonts.d.ts +10 -1
  28. package/plugin/build/withFontsAndroid.d.ts +23 -1
  29. package/plugin/build/withFontsAndroid.js +109 -6
  30. package/plugin/src/utils.ts +14 -0
  31. package/plugin/src/withFonts.ts +12 -1
  32. package/plugin/src/withFontsAndroid.ts +157 -11
  33. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.aar +0 -0
  34. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.aar.md5 +0 -1
  35. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.aar.sha1 +0 -1
  36. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.aar.sha256 +0 -1
  37. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.aar.sha512 +0 -1
  38. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.module.md5 +0 -1
  39. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.module.sha1 +0 -1
  40. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.module.sha256 +0 -1
  41. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.module.sha512 +0 -1
  42. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.pom.md5 +0 -1
  43. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.pom.sha1 +0 -1
  44. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.pom.sha256 +0 -1
  45. package/local-maven-repo/host/exp/exponent/expo.modules.font/13.2.1/expo.modules.font-13.2.1.pom.sha512 +0 -1
  46. /package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1-sources.jar → 13.3.0/expo.modules.font-13.3.0-sources.jar} +0 -0
  47. /package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1-sources.jar.md5 → 13.3.0/expo.modules.font-13.3.0-sources.jar.md5} +0 -0
  48. /package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1-sources.jar.sha1 → 13.3.0/expo.modules.font-13.3.0-sources.jar.sha1} +0 -0
  49. /package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1-sources.jar.sha256 → 13.3.0/expo.modules.font-13.3.0-sources.jar.sha256} +0 -0
  50. /package/local-maven-repo/host/exp/exponent/expo.modules.font/{13.2.1/expo.modules.font-13.2.1-sources.jar.sha512 → 13.3.0/expo.modules.font-13.3.0-sources.jar.sha512} +0 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 13.3.0 — 2025-04-30
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Android] Added support for font weight styles (through XML font definitions) to config plugin. ([#26082](https://github.com/expo/expo/pull/26082) by [@jongbelegen](https://github.com/jongbelegen))
18
+
19
+ ## 13.2.2 — 2025-04-25
20
+
21
+ _This version does not introduce any user-facing changes._
22
+
13
23
  ## 13.2.1 — 2025-04-21
14
24
 
15
25
  _This version does not introduce any user-facing changes._
@@ -4,13 +4,13 @@ plugins {
4
4
  }
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '13.2.1'
7
+ version = '13.3.0'
8
8
 
9
9
  android {
10
10
  namespace "expo.modules.font"
11
11
  defaultConfig {
12
12
  versionCode 29
13
- versionName "13.2.1"
13
+ versionName "13.3.0"
14
14
  }
15
15
  }
16
16
 
@@ -8,7 +8,7 @@
8
8
  "publication": {
9
9
  "groupId": "host.exp.exponent",
10
10
  "artifactId": "expo.modules.font",
11
- "version": "13.2.1",
11
+ "version": "13.3.0",
12
12
  "repository": "local-maven-repo"
13
13
  }
14
14
  }
@@ -0,0 +1 @@
1
+ 8fb055a3e3b59bba7a5290a635b1bd7845a01368
@@ -0,0 +1 @@
1
+ 5274e21b1ff612d9defa01130a701dee5a7c2459b06d7a54b49d0b843d601120
@@ -0,0 +1 @@
1
+ 68cf441a11928d7286379f1985698bdf3925a0dfd852a168968a955f21e32b1d95f1a65a330be805cee8b464cab97ff03a15ad7df77fef45aa25de193ca5b259
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "host.exp.exponent",
5
5
  "module": "expo.modules.font",
6
- "version": "13.2.1",
6
+ "version": "13.3.0",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -24,13 +24,13 @@
24
24
  },
25
25
  "files": [
26
26
  {
27
- "name": "expo.modules.font-13.2.1.aar",
28
- "url": "expo.modules.font-13.2.1.aar",
29
- "size": 23940,
30
- "sha512": "23e09b1c63cedb2c4e2063a509bdd0ecb91c8815cb946ef3707fba50b7ca2f4ac3c20d825847a1885cec0fd3c29dbd153d730acc552a33e5c031928551c707ab",
31
- "sha256": "02e3e22b8a6570b0a9ae2d3fff74bcb898a90480d271d162ef76c2c6415fd763",
32
- "sha1": "8a3e0658bef6d46ddbdfaf891baba11daaadbfc8",
33
- "md5": "c4a5fbec91ef91f550b4a16b8011c1da"
27
+ "name": "expo.modules.font-13.3.0.aar",
28
+ "url": "expo.modules.font-13.3.0.aar",
29
+ "size": 24048,
30
+ "sha512": "68cf441a11928d7286379f1985698bdf3925a0dfd852a168968a955f21e32b1d95f1a65a330be805cee8b464cab97ff03a15ad7df77fef45aa25de193ca5b259",
31
+ "sha256": "5274e21b1ff612d9defa01130a701dee5a7c2459b06d7a54b49d0b843d601120",
32
+ "sha1": "8fb055a3e3b59bba7a5290a635b1bd7845a01368",
33
+ "md5": "40442d97319105928658a150897f05b0"
34
34
  }
35
35
  ]
36
36
  },
@@ -57,13 +57,13 @@
57
57
  ],
58
58
  "files": [
59
59
  {
60
- "name": "expo.modules.font-13.2.1.aar",
61
- "url": "expo.modules.font-13.2.1.aar",
62
- "size": 23940,
63
- "sha512": "23e09b1c63cedb2c4e2063a509bdd0ecb91c8815cb946ef3707fba50b7ca2f4ac3c20d825847a1885cec0fd3c29dbd153d730acc552a33e5c031928551c707ab",
64
- "sha256": "02e3e22b8a6570b0a9ae2d3fff74bcb898a90480d271d162ef76c2c6415fd763",
65
- "sha1": "8a3e0658bef6d46ddbdfaf891baba11daaadbfc8",
66
- "md5": "c4a5fbec91ef91f550b4a16b8011c1da"
60
+ "name": "expo.modules.font-13.3.0.aar",
61
+ "url": "expo.modules.font-13.3.0.aar",
62
+ "size": 24048,
63
+ "sha512": "68cf441a11928d7286379f1985698bdf3925a0dfd852a168968a955f21e32b1d95f1a65a330be805cee8b464cab97ff03a15ad7df77fef45aa25de193ca5b259",
64
+ "sha256": "5274e21b1ff612d9defa01130a701dee5a7c2459b06d7a54b49d0b843d601120",
65
+ "sha1": "8fb055a3e3b59bba7a5290a635b1bd7845a01368",
66
+ "md5": "40442d97319105928658a150897f05b0"
67
67
  }
68
68
  ]
69
69
  },
@@ -77,8 +77,8 @@
77
77
  },
78
78
  "files": [
79
79
  {
80
- "name": "expo.modules.font-13.2.1-sources.jar",
81
- "url": "expo.modules.font-13.2.1-sources.jar",
80
+ "name": "expo.modules.font-13.3.0-sources.jar",
81
+ "url": "expo.modules.font-13.3.0-sources.jar",
82
82
  "size": 3643,
83
83
  "sha512": "04d8003327d266d74c7c2123206ff0c008db185c4fe9ce8518e6c47af1ed8d3118db35e094ceb19451c622318ac15e9fa9eaea609d7fae69cedec95e1656c20a",
84
84
  "sha256": "c029366141433ab312f41026590eb119ef648137f3b6cebeb416531fc0b4cccc",
@@ -0,0 +1 @@
1
+ cc4d40a8195d427839a47a13e22301e42b762f9fb08c7e2cd832a4c8e678af0d
@@ -0,0 +1 @@
1
+ 6306658311812a6a75c4f3151bb8af83e40c13e8600ac1680b6966f491c4d7a34d9850731bac24174ca1b3637aed6b08907d560ac324797c79b4ba9ce44d64fa
@@ -9,7 +9,7 @@
9
9
  <modelVersion>4.0.0</modelVersion>
10
10
  <groupId>host.exp.exponent</groupId>
11
11
  <artifactId>expo.modules.font</artifactId>
12
- <version>13.2.1</version>
12
+ <version>13.3.0</version>
13
13
  <packaging>aar</packaging>
14
14
  <name>expo.modules.font</name>
15
15
  <url>https://github.com/expo/expo</url>
@@ -0,0 +1 @@
1
+ f270178adc59537695ef0a229319fc5259306318
@@ -0,0 +1 @@
1
+ 05fedee066cf10e3e67d493f121429e5bf8fa14cbdf0ac657155292fe1fc544a
@@ -0,0 +1 @@
1
+ f9b0c840091a30da5b1ede1661128fb1160defbcb7cb6d1ee51e1cdd4eaf647e5152d441ef5ce425f126875da53a8907e30177c117290bb2ccb388434d5308dc
@@ -3,11 +3,11 @@
3
3
  <groupId>host.exp.exponent</groupId>
4
4
  <artifactId>expo.modules.font</artifactId>
5
5
  <versioning>
6
- <latest>13.2.1</latest>
7
- <release>13.2.1</release>
6
+ <latest>13.3.0</latest>
7
+ <release>13.3.0</release>
8
8
  <versions>
9
- <version>13.2.1</version>
9
+ <version>13.3.0</version>
10
10
  </versions>
11
- <lastUpdated>20250421220312</lastUpdated>
11
+ <lastUpdated>20250430211456</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- 06143ab0d257dc624f9835145348dbfa
1
+ 0e736e64d8af843891863db86c8fda30
@@ -1 +1 @@
1
- ac9deaae8995da9fdbae4ae7018ec1e5b099246f
1
+ 603e3a4be8441e24654e3088d1e423d4de8f2af7
@@ -1 +1 @@
1
- d363da4f98031768ca2e736f11cf3b6a0532e4df72e0e715aafff487989b1515
1
+ 3232a95e4e0859392951a3f0c6d3c95ff67164999f9fcdb8d4e99b2292cdcaae
@@ -1 +1 @@
1
- 762605d0ba1fa9f023b626a29e2171218017723388f793c4f7815bafe0a4d6ba6dde57db836243eace21f166b13cb0bc82c192cb8575ab898e2d2ffd983891ba
1
+ e4f33387eed6c2e709f4d8f7702f874d639396ea928e38dc4d369ffa2d41d5ec7d4b6b10e11aef3533696f6d5f2afd2ee8978564f0f8f9dc4fec700919b4d496
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-font",
3
- "version": "13.2.1",
3
+ "version": "13.3.0",
4
4
  "description": "Load fonts at runtime and use them in React Native components.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -10,6 +10,8 @@
10
10
  "clean": "expo-module clean",
11
11
  "lint": "expo-module lint",
12
12
  "test": "expo-module test",
13
+ "build:plugin": "expo-module build plugin",
14
+ "test:plugin": "expo-module test plugin",
13
15
  "test:rsc": "jest --config jest-rsc.config.js",
14
16
  "prepare": "expo-module prepare",
15
17
  "prepublishOnly": "expo-module prepublishOnly",
@@ -50,11 +52,11 @@
50
52
  "devDependencies": {
51
53
  "@testing-library/react-native": "^13.1.0",
52
54
  "@types/fontfaceobserver": "^2.1.3",
53
- "expo-module-scripts": "^4.1.2"
55
+ "expo-module-scripts": "^4.1.6"
54
56
  },
55
57
  "peerDependencies": {
56
58
  "expo": "*",
57
59
  "react": "*"
58
60
  },
59
- "gitHead": "9ec1a9d27c26ee62e106c4676e0a8557c303668c"
61
+ "gitHead": "84355076bc31a356aa3d23257f387f221885f53d"
60
62
  }
@@ -1 +1,2 @@
1
+ export declare function toValidAndroidResourceName(value: string): string;
1
2
  export declare function resolveFontPaths(fonts: string[], projectRoot: string): Promise<string[]>;
@@ -3,9 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.toValidAndroidResourceName = toValidAndroidResourceName;
6
7
  exports.resolveFontPaths = resolveFontPaths;
7
8
  const promises_1 = __importDefault(require("fs/promises"));
8
9
  const path_1 = __importDefault(require("path"));
10
+ // rule: File-based resource names must contain only lowercase a-z, 0-9, or underscore
11
+ function toValidAndroidResourceName(value) {
12
+ const valueWithoutFileExtension = path_1.default.parse(value).name;
13
+ const withUnderscores = valueWithoutFileExtension
14
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
15
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2');
16
+ return withUnderscores
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]/g, '_')
19
+ .replace(/_+/g, '_');
20
+ }
9
21
  async function resolveFontPaths(fonts, projectRoot) {
10
22
  const promises = fonts.map(async (p) => {
11
23
  const resolvedPath = path_1.default.resolve(projectRoot, p);
@@ -1,8 +1,17 @@
1
1
  import { type ConfigPlugin } from 'expo/config-plugins';
2
+ export type FontObject = {
3
+ fontFamily: string;
4
+ fontDefinitions: {
5
+ path: string;
6
+ weight: number;
7
+ style?: 'normal' | 'italic' | undefined;
8
+ }[];
9
+ };
10
+ export type Font = string | FontObject;
2
11
  export type FontProps = {
3
12
  fonts?: string[];
4
13
  android?: {
5
- fonts?: string[];
14
+ fonts?: Font[];
6
15
  };
7
16
  ios?: {
8
17
  fonts?: string[];
@@ -1,2 +1,24 @@
1
1
  import { type ConfigPlugin } from 'expo/config-plugins';
2
- export declare const withFontsAndroid: ConfigPlugin<string[]>;
2
+ import type { Font, FontObject } from './withFonts';
3
+ export declare const withFontsAndroid: ConfigPlugin<Font[]>;
4
+ type GroupedFontObject = Record<string, FontObject['fontDefinitions']>;
5
+ export declare function groupByFamily(array: FontObject[]): GroupedFontObject;
6
+ export declare function getXmlSpecs(fontsDir: string, xmlFontObjects: GroupedFontObject): {
7
+ path: string;
8
+ xml: {
9
+ 'font-family': {
10
+ $: {
11
+ 'xmlns:app': string;
12
+ };
13
+ font: {
14
+ $: {
15
+ 'app:font': string;
16
+ 'app:fontStyle': "normal" | "italic";
17
+ 'app:fontWeight': string;
18
+ };
19
+ }[];
20
+ };
21
+ };
22
+ }[];
23
+ export declare function generateFontManagerCalls(xmlFontObjects: GroupedFontObject, language: 'java' | 'kt'): string[];
24
+ export {};
@@ -4,19 +4,123 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.withFontsAndroid = void 0;
7
+ exports.groupByFamily = groupByFamily;
8
+ exports.getXmlSpecs = getXmlSpecs;
9
+ exports.generateFontManagerCalls = generateFontManagerCalls;
10
+ const codeMod_1 = require("@expo/config-plugins/build/android/codeMod");
11
+ const generateCode_1 = require("@expo/config-plugins/build/utils/generateCode");
7
12
  const config_plugins_1 = require("expo/config-plugins");
8
13
  const promises_1 = __importDefault(require("fs/promises"));
14
+ const os_1 = __importDefault(require("os"));
9
15
  const path_1 = __importDefault(require("path"));
10
16
  const utils_1 = require("./utils");
17
+ const assetsFontsFir = 'app/src/main/assets/fonts';
18
+ const resourcesFontsDir = 'app/src/main/res/font';
11
19
  const withFontsAndroid = (config, fonts) => {
20
+ const assetFontPaths = fonts.filter((it) => typeof it === 'string');
21
+ config = copyFontsToDir(config, assetFontPaths, assetsFontsFir);
22
+ const xmlFonts = fonts.filter((it) => typeof it === 'object');
23
+ config = addXmlFonts(config, xmlFonts);
24
+ return config;
25
+ };
26
+ exports.withFontsAndroid = withFontsAndroid;
27
+ function groupByFamily(array) {
28
+ return array.reduce((result, item) => {
29
+ const keyValue = item['fontFamily'];
30
+ result[keyValue] ||= [];
31
+ result[keyValue].push(...item.fontDefinitions);
32
+ return result;
33
+ }, {});
34
+ }
35
+ function addXmlFonts(config, xmlFontObjects) {
36
+ const fontsByFamily = groupByFamily(xmlFontObjects);
37
+ const fontPaths = Object.values(fontsByFamily)
38
+ .map((font) => font.map((it) => it.path))
39
+ .flat();
40
+ config = copyFontsToDir(config, fontPaths, resourcesFontsDir, (filenameWithExt) => {
41
+ const filename = (0, utils_1.toValidAndroidResourceName)(filenameWithExt);
42
+ const ext = path_1.default.extname(filenameWithExt);
43
+ return `${filename}${ext}`;
44
+ });
45
+ config = addFontXmlToMainApplication(config, fontsByFamily);
46
+ return (0, config_plugins_1.withDangerousMod)(config, [
47
+ 'android',
48
+ async (config) => {
49
+ const fontsDir = path_1.default.join(config.modRequest.platformProjectRoot, 'app/src/main/res/font');
50
+ const xmlSpecs = getXmlSpecs(fontsDir, fontsByFamily);
51
+ await Promise.all(xmlSpecs.map(config_plugins_1.XML.writeXMLAsync));
52
+ return config;
53
+ },
54
+ ]);
55
+ }
56
+ const resourceNameConflictAvoidancePrefix = 'xml_';
57
+ function getXmlSpecs(fontsDir, xmlFontObjects) {
58
+ return Object.entries(xmlFontObjects).map(([fontFamily, fontDefinitions]) => {
59
+ const filePath = path_1.default.join(fontsDir, `${resourceNameConflictAvoidancePrefix + (0, utils_1.toValidAndroidResourceName)(fontFamily)}.xml`);
60
+ // each font family has one xml resource file with potentially multiple font definitions
61
+ // the font files (e.g. ttf) at `path` are copied to res/font
62
+ // with their name changed to be a valid resource and referenced in the xml file
63
+ return {
64
+ path: filePath,
65
+ xml: {
66
+ 'font-family': {
67
+ // using `app` namespace for better compat:
68
+ // https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml#using-support-lib
69
+ $: {
70
+ 'xmlns:app': 'http://schemas.android.com/apk/res-auto',
71
+ },
72
+ font: fontDefinitions.map((definition) => {
73
+ return {
74
+ $: {
75
+ 'app:font': `@font/${(0, utils_1.toValidAndroidResourceName)(definition.path)}`,
76
+ 'app:fontStyle': definition.style || 'normal',
77
+ 'app:fontWeight': String(definition.weight),
78
+ },
79
+ };
80
+ }),
81
+ },
82
+ },
83
+ };
84
+ });
85
+ }
86
+ function addFontXmlToMainApplication(config, xmlFontObjects) {
87
+ return (0, config_plugins_1.withMainApplication)(config, (config) => {
88
+ const { modResults, modResults: { language }, } = config;
89
+ modResults.contents = (0, codeMod_1.addImports)(modResults.contents, ['com.facebook.react.common.assets.ReactFontManager'], language === 'java');
90
+ const fontManagerCalls = generateFontManagerCalls(xmlFontObjects, language).join(os_1.default.EOL);
91
+ const withInit = (0, generateCode_1.mergeContents)({
92
+ src: modResults.contents,
93
+ comment: ' //',
94
+ tag: 'xml-fonts-init',
95
+ offset: 1,
96
+ anchor: /super\.onCreate\(\)/,
97
+ newSrc: fontManagerCalls,
98
+ });
99
+ return {
100
+ ...config,
101
+ modResults: {
102
+ ...modResults,
103
+ contents: withInit.contents,
104
+ },
105
+ };
106
+ });
107
+ }
108
+ function generateFontManagerCalls(xmlFontObjects, language) {
109
+ const lineEnding = language === 'java' ? ';' : '';
110
+ const indent = ' ';
111
+ return Object.keys(xmlFontObjects).map((family) => `${indent}ReactFontManager.getInstance().addCustomFont(this, "${family}", R.font.${resourceNameConflictAvoidancePrefix + (0, utils_1.toValidAndroidResourceName)(family)})${lineEnding}`);
112
+ }
113
+ function copyFontsToDir(config, paths, inAppDestination, filenameProcessor = (filenameWithExt) => filenameWithExt) {
12
114
  return (0, config_plugins_1.withDangerousMod)(config, [
13
115
  'android',
14
116
  async (config) => {
15
- const resolvedFonts = await (0, utils_1.resolveFontPaths)(fonts, config.modRequest.projectRoot);
117
+ const fontsDir = path_1.default.join(config.modRequest.platformProjectRoot, inAppDestination);
118
+ await promises_1.default.mkdir(fontsDir, { recursive: true });
119
+ const resolvedFonts = await (0, utils_1.resolveFontPaths)(paths, config.modRequest.projectRoot);
16
120
  await Promise.all(resolvedFonts.map(async (asset) => {
17
- const fontsDir = path_1.default.join(config.modRequest.platformProjectRoot, 'app/src/main/assets/fonts');
18
- await promises_1.default.mkdir(fontsDir, { recursive: true });
19
- const output = path_1.default.join(fontsDir, path_1.default.basename(asset));
121
+ const filenameWithExt = path_1.default.basename(asset);
122
+ const outputFileName = filenameProcessor(filenameWithExt);
123
+ const output = path_1.default.join(fontsDir, outputFileName);
20
124
  if (output.endsWith('.ttf') || output.endsWith('.otf')) {
21
125
  await promises_1.default.copyFile(asset, output);
22
126
  }
@@ -24,5 +128,4 @@ const withFontsAndroid = (config, fonts) => {
24
128
  return config;
25
129
  },
26
130
  ]);
27
- };
28
- exports.withFontsAndroid = withFontsAndroid;
131
+ }
@@ -1,6 +1,20 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
 
4
+ // rule: File-based resource names must contain only lowercase a-z, 0-9, or underscore
5
+ export function toValidAndroidResourceName(value: string) {
6
+ const valueWithoutFileExtension = path.parse(value).name;
7
+
8
+ const withUnderscores = valueWithoutFileExtension
9
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
10
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1_$2');
11
+
12
+ return withUnderscores
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9]/g, '_')
15
+ .replace(/_+/g, '_');
16
+ }
17
+
4
18
  export async function resolveFontPaths(fonts: string[], projectRoot: string) {
5
19
  const promises = fonts.map(async (p) => {
6
20
  const resolvedPath = path.resolve(projectRoot, p);
@@ -5,10 +5,21 @@ import { withFontsIos } from './withFontsIos';
5
5
 
6
6
  const pkg = require('expo-font/package.json');
7
7
 
8
+ export type FontObject = {
9
+ fontFamily: string;
10
+ fontDefinitions: {
11
+ path: string;
12
+ weight: number;
13
+ style?: 'normal' | 'italic' | undefined;
14
+ }[];
15
+ };
16
+
17
+ export type Font = string | FontObject;
18
+
8
19
  export type FontProps = {
9
20
  fonts?: string[];
10
21
  android?: {
11
- fonts?: string[];
22
+ fonts?: Font[];
12
23
  };
13
24
  ios?: {
14
25
  fonts?: string[];
@@ -1,22 +1,168 @@
1
- import { type ConfigPlugin, withDangerousMod } from 'expo/config-plugins';
1
+ import { addImports } from '@expo/config-plugins/build/android/codeMod';
2
+ import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
3
+ import { type ExpoConfig } from '@expo/config-types';
4
+ import { type ConfigPlugin, withDangerousMod, withMainApplication, XML } from 'expo/config-plugins';
2
5
  import fs from 'fs/promises';
6
+ import os from 'os';
3
7
  import path from 'path';
4
8
 
5
- import { resolveFontPaths } from './utils';
9
+ import { resolveFontPaths, toValidAndroidResourceName } from './utils';
10
+ import type { Font, FontObject } from './withFonts';
11
+
12
+ const assetsFontsFir = 'app/src/main/assets/fonts';
13
+ const resourcesFontsDir = 'app/src/main/res/font';
14
+
15
+ export const withFontsAndroid: ConfigPlugin<Font[]> = (config, fonts) => {
16
+ const assetFontPaths = fonts.filter((it) => typeof it === 'string');
17
+ config = copyFontsToDir(config, assetFontPaths, assetsFontsFir);
18
+
19
+ const xmlFonts = fonts.filter((it) => typeof it === 'object');
20
+ config = addXmlFonts(config, xmlFonts);
21
+
22
+ return config;
23
+ };
24
+
25
+ type GroupedFontObject = Record<string, FontObject['fontDefinitions']>;
26
+
27
+ export function groupByFamily(array: FontObject[]): GroupedFontObject {
28
+ return array.reduce<GroupedFontObject>((result, item) => {
29
+ const keyValue = item['fontFamily'];
30
+ result[keyValue] ||= [];
31
+ result[keyValue].push(...item.fontDefinitions);
32
+ return result;
33
+ }, {});
34
+ }
35
+
36
+ function addXmlFonts(config: ExpoConfig, xmlFontObjects: FontObject[]) {
37
+ const fontsByFamily = groupByFamily(xmlFontObjects);
38
+
39
+ const fontPaths = Object.values(fontsByFamily)
40
+ .map((font) => font.map((it) => it.path))
41
+ .flat();
42
+
43
+ config = copyFontsToDir(config, fontPaths, resourcesFontsDir, (filenameWithExt) => {
44
+ const filename = toValidAndroidResourceName(filenameWithExt);
45
+ const ext = path.extname(filenameWithExt);
46
+ return `${filename}${ext}`;
47
+ });
48
+
49
+ config = addFontXmlToMainApplication(config, fontsByFamily);
6
50
 
7
- export const withFontsAndroid: ConfigPlugin<string[]> = (config, fonts) => {
8
51
  return withDangerousMod(config, [
9
52
  'android',
10
53
  async (config) => {
11
- const resolvedFonts = await resolveFontPaths(fonts, config.modRequest.projectRoot);
54
+ const fontsDir = path.join(config.modRequest.platformProjectRoot, 'app/src/main/res/font');
55
+
56
+ const xmlSpecs = getXmlSpecs(fontsDir, fontsByFamily);
57
+
58
+ await Promise.all(xmlSpecs.map(XML.writeXMLAsync));
59
+
60
+ return config;
61
+ },
62
+ ]);
63
+ }
64
+
65
+ const resourceNameConflictAvoidancePrefix = 'xml_';
66
+
67
+ export function getXmlSpecs(fontsDir: string, xmlFontObjects: GroupedFontObject) {
68
+ return Object.entries(xmlFontObjects).map(([fontFamily, fontDefinitions]) => {
69
+ const filePath = path.join(
70
+ fontsDir,
71
+ `${resourceNameConflictAvoidancePrefix + toValidAndroidResourceName(fontFamily)}.xml`
72
+ );
73
+ // each font family has one xml resource file with potentially multiple font definitions
74
+ // the font files (e.g. ttf) at `path` are copied to res/font
75
+ // with their name changed to be a valid resource and referenced in the xml file
76
+ return {
77
+ path: filePath,
78
+ xml: {
79
+ 'font-family': {
80
+ // using `app` namespace for better compat:
81
+ // https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml#using-support-lib
82
+ $: {
83
+ 'xmlns:app': 'http://schemas.android.com/apk/res-auto',
84
+ },
85
+ font: fontDefinitions.map((definition) => {
86
+ return {
87
+ $: {
88
+ 'app:font': `@font/${toValidAndroidResourceName(definition.path)}`,
89
+ 'app:fontStyle': definition.style || 'normal',
90
+ 'app:fontWeight': String(definition.weight),
91
+ },
92
+ };
93
+ }),
94
+ },
95
+ },
96
+ };
97
+ });
98
+ }
99
+
100
+ function addFontXmlToMainApplication(config: ExpoConfig, xmlFontObjects: GroupedFontObject) {
101
+ return withMainApplication(config, (config) => {
102
+ const {
103
+ modResults,
104
+ modResults: { language },
105
+ } = config;
106
+
107
+ modResults.contents = addImports(
108
+ modResults.contents,
109
+ ['com.facebook.react.common.assets.ReactFontManager'],
110
+ language === 'java'
111
+ );
112
+
113
+ const fontManagerCalls = generateFontManagerCalls(xmlFontObjects, language).join(os.EOL);
114
+
115
+ const withInit = mergeContents({
116
+ src: modResults.contents,
117
+ comment: ' //',
118
+ tag: 'xml-fonts-init',
119
+ offset: 1,
120
+ anchor: /super\.onCreate\(\)/,
121
+ newSrc: fontManagerCalls,
122
+ });
123
+
124
+ return {
125
+ ...config,
126
+ modResults: {
127
+ ...modResults,
128
+ contents: withInit.contents,
129
+ },
130
+ };
131
+ });
132
+ }
133
+
134
+ export function generateFontManagerCalls(
135
+ xmlFontObjects: GroupedFontObject,
136
+ language: 'java' | 'kt'
137
+ ) {
138
+ const lineEnding = language === 'java' ? ';' : '';
139
+ const indent = ' ';
140
+
141
+ return Object.keys(xmlFontObjects).map(
142
+ (family) =>
143
+ `${indent}ReactFontManager.getInstance().addCustomFont(this, "${family}", R.font.${resourceNameConflictAvoidancePrefix + toValidAndroidResourceName(family)})${lineEnding}`
144
+ );
145
+ }
146
+
147
+ function copyFontsToDir(
148
+ config: ExpoConfig,
149
+ paths: string[],
150
+ inAppDestination: typeof assetsFontsFir | typeof resourcesFontsDir,
151
+ filenameProcessor = (filenameWithExt: string) => filenameWithExt
152
+ ) {
153
+ return withDangerousMod(config, [
154
+ 'android',
155
+ async (config) => {
156
+ const fontsDir = path.join(config.modRequest.platformProjectRoot, inAppDestination);
157
+ await fs.mkdir(fontsDir, { recursive: true });
158
+
159
+ const resolvedFonts = await resolveFontPaths(paths, config.modRequest.projectRoot);
160
+
12
161
  await Promise.all(
13
162
  resolvedFonts.map(async (asset) => {
14
- const fontsDir = path.join(
15
- config.modRequest.platformProjectRoot,
16
- 'app/src/main/assets/fonts'
17
- );
18
- await fs.mkdir(fontsDir, { recursive: true });
19
- const output = path.join(fontsDir, path.basename(asset));
163
+ const filenameWithExt = path.basename(asset);
164
+ const outputFileName = filenameProcessor(filenameWithExt);
165
+ const output = path.join(fontsDir, outputFileName);
20
166
  if (output.endsWith('.ttf') || output.endsWith('.otf')) {
21
167
  await fs.copyFile(asset, output);
22
168
  }
@@ -25,4 +171,4 @@ export const withFontsAndroid: ConfigPlugin<string[]> = (config, fonts) => {
25
171
  return config;
26
172
  },
27
173
  ]);
28
- };
174
+ }
@@ -1 +0,0 @@
1
- 8a3e0658bef6d46ddbdfaf891baba11daaadbfc8
@@ -1 +0,0 @@
1
- 02e3e22b8a6570b0a9ae2d3fff74bcb898a90480d271d162ef76c2c6415fd763
@@ -1 +0,0 @@
1
- 23e09b1c63cedb2c4e2063a509bdd0ecb91c8815cb946ef3707fba50b7ca2f4ac3c20d825847a1885cec0fd3c29dbd153d730acc552a33e5c031928551c707ab
@@ -1 +0,0 @@
1
- 744c03e7065961577e85e6a6906545ba5f715369
@@ -1 +0,0 @@
1
- 67b68f0ded24f2aaf72c4e89269c04f815ab133341e300b6dbcb7ea8c0211b08
@@ -1 +0,0 @@
1
- fd4147e6cd7d0153a8025a673cfe3eaeb9125423c425b616afdf6585067ea799132c929c9cb0c39d13632e7729b586115a1999a13f13afeb93f216a0b10ecb05
@@ -1 +0,0 @@
1
- a9aed5a8695cde23f07557b07b37c99ee42698f0
@@ -1 +0,0 @@
1
- f3f3b799afdcda81128d46eb305131cdbadb5af9ae0076b6d921e9dc8f89d929
@@ -1 +0,0 @@
1
- 29e2e1c0252322dfbc167060ac6f9424d8df0cc699e4e3c18108fa1dfbb36dd5ae1fad14e0b74931174dfb3489282233343860f00a55a23d29366bde0575b48b