react-native-nano-icons 0.1.8 → 0.2.1

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 (116) hide show
  1. package/README.md +64 -2
  2. package/android/src/main/java/com/nanoicons/NanoIconsFontLoaderModule.kt +63 -0
  3. package/android/src/main/java/com/nanoicons/NanoIconsPackage.kt +19 -2
  4. package/ios/NanoIconView.h +5 -0
  5. package/ios/NanoIconView.mm +35 -9
  6. package/ios/NanoIconsFontLoader.h +11 -0
  7. package/ios/NanoIconsFontLoader.mm +110 -0
  8. package/lib/commonjs/cli/build.d.ts +3 -0
  9. package/lib/commonjs/cli/build.js +8 -4
  10. package/lib/commonjs/cli/config.d.ts +1 -0
  11. package/lib/commonjs/cli/config.js +10 -0
  12. package/lib/commonjs/cli/expoConfig.d.ts +8 -0
  13. package/lib/commonjs/cli/expoConfig.js +34 -0
  14. package/lib/commonjs/cli/index.d.ts +2 -1
  15. package/lib/commonjs/cli/index.js +4 -1
  16. package/lib/commonjs/cli/link.js +32 -22
  17. package/lib/commonjs/plugin/src/types.d.ts +9 -0
  18. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.d.ts +5 -0
  19. package/lib/commonjs/plugin/src/withNanoIconsFontLinking.js +24 -10
  20. package/lib/commonjs/scripts/cli.js +23 -11
  21. package/lib/commonjs/src/core/pipeline/config.d.ts +1 -0
  22. package/lib/commonjs/src/core/pipeline/managers.js +1 -2
  23. package/lib/commonjs/src/core/pipeline/run.js +1 -0
  24. package/lib/commonjs/src/core/svg/svg_dom.d.ts +1 -0
  25. package/lib/commonjs/src/core/svg/svg_dom.js +22 -17
  26. package/lib/commonjs/src/core/svg/svg_pathops.js +30 -16
  27. package/lib/commonjs/src/core/types.d.ts +3 -0
  28. package/lib/module/const/codegenPrimitives.js +2 -0
  29. package/lib/module/const/codegenPrimitives.js.map +1 -0
  30. package/lib/module/core/font/compile.js.map +1 -1
  31. package/lib/module/core/pipeline/config.js.map +1 -1
  32. package/lib/module/core/pipeline/managers.js.map +1 -1
  33. package/lib/module/core/pipeline/run.js +4 -1
  34. package/lib/module/core/pipeline/run.js.map +1 -1
  35. package/lib/module/core/svg/layers.js.map +1 -1
  36. package/lib/module/core/svg/svg_dom.js +19 -19
  37. package/lib/module/core/svg/svg_dom.js.map +1 -1
  38. package/lib/module/core/svg/svg_pathops.js.map +1 -1
  39. package/lib/module/createNanoIconsSet.js +2 -2
  40. package/lib/module/createNanoIconsSet.js.map +1 -1
  41. package/lib/module/createNanoIconsSet.native.js +43 -16
  42. package/lib/module/createNanoIconsSet.native.js.map +1 -1
  43. package/lib/module/createNanoIconsSet.shared.js +49 -20
  44. package/lib/module/createNanoIconsSet.shared.js.map +1 -1
  45. package/lib/module/createNanoIconsSet.web.js +78 -0
  46. package/lib/module/createNanoIconsSet.web.js.map +1 -0
  47. package/lib/module/loadDynamicFont.js +118 -0
  48. package/lib/module/loadDynamicFont.js.map +1 -0
  49. package/lib/module/specs/NanoIconViewNativeComponent.ts +9 -8
  50. package/lib/module/specs/NativeNanoIconsFontLoader.js +7 -0
  51. package/lib/module/specs/NativeNanoIconsFontLoader.js.map +1 -0
  52. package/lib/module/utils/glyphRuntime.js +37 -0
  53. package/lib/module/utils/glyphRuntime.js.map +1 -0
  54. package/lib/typescript/__tests__/glyphRuntime.unit.test.d.ts +2 -0
  55. package/lib/typescript/__tests__/glyphRuntime.unit.test.d.ts.map +1 -0
  56. package/lib/typescript/__tests__/link.unit.test.d.ts +3 -0
  57. package/lib/typescript/__tests__/link.unit.test.d.ts.map +1 -0
  58. package/lib/typescript/__tests__/loadDynamicFont.unit.test.d.ts +2 -0
  59. package/lib/typescript/__tests__/loadDynamicFont.unit.test.d.ts.map +1 -0
  60. package/lib/typescript/cli/build.d.ts +3 -0
  61. package/lib/typescript/cli/build.d.ts.map +1 -1
  62. package/lib/typescript/cli/link.d.ts +12 -0
  63. package/lib/typescript/cli/link.d.ts.map +1 -0
  64. package/lib/typescript/src/const/codegenPrimitives.d.ts +3 -0
  65. package/lib/typescript/src/const/codegenPrimitives.d.ts.map +1 -0
  66. package/lib/typescript/src/core/font/compile.d.ts.map +1 -1
  67. package/lib/typescript/src/core/pipeline/config.d.ts +1 -0
  68. package/lib/typescript/src/core/pipeline/config.d.ts.map +1 -1
  69. package/lib/typescript/src/core/pipeline/managers.d.ts.map +1 -1
  70. package/lib/typescript/src/core/pipeline/run.d.ts.map +1 -1
  71. package/lib/typescript/src/core/svg/svg_dom.d.ts +1 -0
  72. package/lib/typescript/src/core/svg/svg_dom.d.ts.map +1 -1
  73. package/lib/typescript/src/core/svg/svg_pathops.d.ts.map +1 -1
  74. package/lib/typescript/src/core/types.d.ts +3 -0
  75. package/lib/typescript/src/core/types.d.ts.map +1 -1
  76. package/lib/typescript/src/createNanoIconsSet.d.ts +1 -0
  77. package/lib/typescript/src/createNanoIconsSet.d.ts.map +1 -1
  78. package/lib/typescript/src/createNanoIconsSet.native.d.ts +1 -0
  79. package/lib/typescript/src/createNanoIconsSet.native.d.ts.map +1 -1
  80. package/lib/typescript/src/createNanoIconsSet.shared.d.ts +11 -0
  81. package/lib/typescript/src/createNanoIconsSet.shared.d.ts.map +1 -1
  82. package/lib/typescript/src/createNanoIconsSet.web.d.ts +7 -0
  83. package/lib/typescript/src/createNanoIconsSet.web.d.ts.map +1 -0
  84. package/lib/typescript/src/loadDynamicFont.d.ts +28 -0
  85. package/lib/typescript/src/loadDynamicFont.d.ts.map +1 -0
  86. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts +9 -8
  87. package/lib/typescript/src/specs/NanoIconViewNativeComponent.d.ts.map +1 -1
  88. package/lib/typescript/src/specs/NativeNanoIconsFontLoader.d.ts +8 -0
  89. package/lib/typescript/src/specs/NativeNanoIconsFontLoader.d.ts.map +1 -0
  90. package/lib/typescript/src/types.d.ts +7 -1
  91. package/lib/typescript/src/types.d.ts.map +1 -1
  92. package/lib/typescript/src/utils/glyphRuntime.d.ts +18 -0
  93. package/lib/typescript/src/utils/glyphRuntime.d.ts.map +1 -0
  94. package/package.json +8 -5
  95. package/plugin/src/types.ts +9 -0
  96. package/plugin/src/withNanoIconsFontLinking.ts +29 -10
  97. package/react-native-nano-icons.podspec +1 -1
  98. package/scripts/cli.ts +31 -11
  99. package/src/const/codegenPrimitives.ts +14 -0
  100. package/src/core/font/compile.ts +1 -2
  101. package/src/core/pipeline/config.ts +1 -0
  102. package/src/core/pipeline/managers.ts +5 -10
  103. package/src/core/pipeline/run.ts +11 -6
  104. package/src/core/svg/layers.ts +4 -4
  105. package/src/core/svg/svg_dom.ts +26 -23
  106. package/src/core/svg/svg_pathops.ts +50 -24
  107. package/src/core/types.ts +10 -2
  108. package/src/createNanoIconsSet.native.tsx +78 -26
  109. package/src/createNanoIconsSet.shared.tsx +93 -40
  110. package/src/createNanoIconsSet.tsx +9 -1
  111. package/src/createNanoIconsSet.web.tsx +109 -0
  112. package/src/loadDynamicFont.ts +162 -0
  113. package/src/specs/NanoIconViewNativeComponent.ts +9 -8
  114. package/src/specs/NativeNanoIconsFontLoader.ts +11 -0
  115. package/src/types.ts +5 -1
  116. package/src/utils/glyphRuntime.ts +46 -0
package/README.md CHANGED
@@ -42,8 +42,9 @@ That’s it 🔬⚡️
42
42
  - [🎨 Multicolor Icons](#-multicolor-icons)
43
43
  - [📊 Performance](#-performance)
44
44
  - [⚠️ Known Limitations](#%EF%B8%8F-known-limitations)
45
- - [🔧 Font Generation Pipeline](#-how-it-works)
45
+ - [🔧 Font Generation Pipeline](#-font-generation-pipeline)
46
46
  - [🤝 Contributing](#-contributing)
47
+ - [License](#license)
47
48
 
48
49
  ---
49
50
 
@@ -54,6 +55,7 @@ That’s it 🔬⚡️
54
55
  - [x] Android API 24+
55
56
  - [x] Web
56
57
  - [x] Expo Go
58
+ - [x] tvOS 15.1+
57
59
 
58
60
  ---
59
61
 
@@ -118,6 +120,7 @@ The plugin accepts an object with an `iconSets` array, allowing you to generate
118
120
  | `outputDir` | `string` | No | `../nanoicons` | Path where the `.ttf` and `.json` artifacts will be saved. Defaults to a sibling `nanoicons` folder relative to the input. |
119
121
  | `upm` | `number` | No | `1024` | Units Per Em. Defines the resolution of the font grid. |
120
122
  | `startUnicode` | `string` | No | `0xe900` | The starting Hex Unicode point for the first icon glyph. |
123
+ | `linking` | `'static' \| 'dynamic'` | No | `'static'` | Delivery mode for the generated TTF. `'static'` bundles it into the native app. `'dynamic'` excludes it from native linking so the host app can deliver it at runtime (i.e. via OTA update). See [Dynamic linking](#dynamic-linking-expo-ota-updates-support). |
121
124
 
122
125
  <details>
123
126
  <summary>Default Dir Path Behavior</summary>
@@ -203,10 +206,68 @@ export default function App() {
203
206
  | `testID` | `string` | — | Test identifier for e2e testing frameworks. |
204
207
  | `ref` | `Ref<View>` | — | Ref to the underlying native view. |
205
208
 
209
+ ### Dynamic linking (Expo OTA updates support)
210
+ TL;DR the default static linking is best for most use cases as it does not affect JS bundle size at all, but if you have an [OTA updates workflow](https://expo.dev/solutions/eas-ota-updates) and make changes to your icons frequently, you can opt out of native bundling and register a particular iconSet font at runtime explicitly.
211
+
212
+ By default, generated TTFs are bundled into the native app at build/link time. Set `linking: 'dynamic'` to opt out: the build still produces the `.ttf` and `.glyphmap.json`, but then OTA workflows will ship icon outside the native binary - you deliver the file and the library will register it at runtime.
213
+
214
+ **Config**
215
+
216
+ ```JSON
217
+ {
218
+ "iconSets": [
219
+ {
220
+ "inputDir": "./assets/icons/dynamic-ota-icons",
221
+ "linking": "dynamic"
222
+ }
223
+ ]
224
+ }
225
+ ```
226
+
227
+ > [!NOTE]
228
+ > You can mix both linking modes in the same config — some icon sets can be statically bundled and others delivered dynamically. Each entry in `iconSets` is independent.
229
+
230
+ **Runtime**
231
+
232
+ Pass the font as the second argument to `createNanoIconSet`. The library registers it under the family name at runtime.
233
+
234
+ ```TypeScript
235
+ import { createNanoIconSet } from "react-native-nano-icons";
236
+ import glyphMap from "./dynamic-ota-icons.glyphmap.json";
237
+
238
+ export const Icon = createNanoIconSet(glyphMap, require("./dynamic-ota-icons.ttf"));
239
+ // or: createNanoIconSet(glyphMap, { uri: "https://cdn.example.com/remote-nano-icons.ttf" })
240
+ ```
241
+
242
+ > [!NOTE]
243
+ > In [Expo Go](https://expo.dev/go), the native font loader is unavailable, but you can still see real icons by loading the font manually via [`expo-font`](https://docs.expo.dev/versions/latest/sdk/font/) — use the value of `glyphMap.m.f` as the family name key. [Once you move to a development build](https://docs.expo.dev/develop/development-builds/expo-go-to-dev-build/), the library registers the font automatically and you can remove the `expo-font` setup.
244
+
245
+ > If a dynamic glyphmap gets no font, icons render as tofu until one is registered (with a dev warning).
246
+
206
247
  ### 5. Font Regeneration
207
248
 
208
249
  **The build script detects changes in path and contents of the SVGs** in your input directory based on a fingerprint hash. If anything changes (file names, SVG attributes/nodes) or the output font/glyphmap files are deleted, the icon set is regenerated during `prebuild` or manual script run.
209
250
 
251
+ ### Regenerating dynamic fonts only (useful for an OTA update) ☁️
252
+
253
+ When your `dynamic` icons change and you want to ship them via OTA update, you don't need to run a full `expo prebuild` and native rebuild. Use `--dynamic` to regenerate only the dynamic sets:
254
+
255
+ ```sh
256
+ # run from your app root
257
+ npx react-native-nano-icons --path path/to/.nanoicons.json --dynamic
258
+ ```
259
+
260
+
261
+ > [!TIP]
262
+ > Using Expo CNG with the app config and expo plugin instead of `.nanoicons.json` ? Just add `--app-config` flag to use your plugin input setup instead:
263
+ > ```sh
264
+ > # run from your app root
265
+ > npx react-native-nano-icons --dynamic --app-config
266
+ > ```
267
+ > This reads your config directly from `app.json` / `app.config.js` / `app.config.ts` (no separate `.nanoicons.json` needed).
268
+
269
+ The CLI rebuilds only the sets defined with `linking: "dynamic"`, and skips all native linking. Commit the updated `.ttf` and `.glyphmap.json` and push your OTA update as usual ☁️ 🚀
270
+
210
271
  ---
211
272
 
212
273
  ## 🎨 Multicolor Icons
@@ -278,6 +339,7 @@ The chart shows time in milliseconds across three phases: **JS Thread** (JavaScr
278
339
  ## ⚠️ Known Limitations
279
340
 
280
341
  - SVG `<filter>` and `<mask>` elements are not supported — font glyphs cannot represent these effects.
342
+ - Embedded raster images (`<image>` elements, e.g. base64-encoded bitmaps inside an SVG) are not supported — only vector geometry can be converted to glyphs.
281
343
  - Only `*.svg` input files are supported.
282
344
 
283
345
  ---
@@ -298,7 +360,7 @@ At build time, the pipeline processes your SVG directory through four stages:
298
360
  4. **Font compilation** — Layers are compiled into a standard `.ttf` font file, with each layer mapped to a private-use Unicode codepoint.
299
361
  5. **Glyphmap generation** — A compact `.glyphmap.json` is created, mapping icon names to their codepoints, default colors, and metrics.
300
362
 
301
- At runtime, the native component stacks glyph layers at the same position — one `drawGlyphs` call per layer via [CoreText](https://developer.apple.com/documentation/coretext/) (iOS) or `drawText` via [Canvas](<https://developer.android.com/reference/android/graphics/Canvas#drawText(java.lang.String,%20float,%20float,%20android.graphics.Paint)>) (Android). On web and Expo Go, a pure `react-native` fallback uses stacked `<Text>` elements.
363
+ At runtime, the native component stacks glyph layers at the same position — one `drawGlyphs` call per layer via [CoreText](https://developer.apple.com/documentation/coretext/) (iOS) or `drawText` via [Canvas](<https://developer.android.com/reference/android/graphics/Canvas#drawText(java.lang.String,%20float,%20float,%20android.graphics.Paint)>) (Android). On the web, icons render as stacked inline `<span>` elements. In Expo Go, a pure `react-native` fallback uses stacked `<Text>` elements.
302
364
 
303
365
  ---
304
366
 
@@ -0,0 +1,63 @@
1
+ package com.nanoicons
2
+
3
+ import android.graphics.Typeface
4
+ import android.net.Uri
5
+ import com.facebook.react.bridge.Promise
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.common.assets.ReactFontManager
8
+ import java.io.File
9
+ import java.io.InputStream
10
+ import java.net.URL
11
+
12
+ /**
13
+ * Registers a dynamically-linked (OTA) font at runtime so the NanoIconView can
14
+ * resolve it by family name via ReactFontManager — the same registry RN fonts use.
15
+ *
16
+ * Reads the bytes at `uri` (file:// / http(s):// / content:// / plain path),
17
+ * writes them to a cache file, builds a Typeface, and registers it under `family`.
18
+ * Caching/versioning of remote fonts is intentionally out of scope.
19
+ */
20
+ class NanoIconsFontLoaderModule(reactContext: ReactApplicationContext) :
21
+ NativeNanoIconsFontLoaderSpec(reactContext) {
22
+
23
+ override fun getName(): String = NAME
24
+
25
+ override fun registerFont(family: String, uri: String, promise: Promise) {
26
+ try {
27
+ val cacheFile = File.createTempFile("nanoicon_", ".ttf", reactApplicationContext.cacheDir)
28
+ openStream(uri).use { input ->
29
+ cacheFile.outputStream().use { output -> input.copyTo(output) }
30
+ }
31
+
32
+ val typeface = Typeface.createFromFile(cacheFile)
33
+ ReactFontManager.getInstance().setTypeface(family, Typeface.NORMAL, typeface)
34
+ promise.resolve(true)
35
+ } catch (e: Exception) {
36
+ promise.reject(
37
+ "E_NANOICONS_FONT_REGISTER",
38
+ "Failed to register font \"$family\" from $uri: ${e.message}",
39
+ e
40
+ )
41
+ }
42
+ }
43
+
44
+ private fun openStream(uri: String): InputStream =
45
+ when {
46
+ uri.startsWith("content://") ->
47
+ reactApplicationContext.contentResolver.openInputStream(Uri.parse(uri))
48
+ ?: throw IllegalStateException("Cannot open content uri: $uri")
49
+ uri.startsWith("file://") ||
50
+ uri.startsWith("http://") ||
51
+ uri.startsWith("https://") -> URL(uri).openStream()
52
+ else -> {
53
+ val resources = reactApplicationContext.resources
54
+ val resId =
55
+ resources.getIdentifier(uri, "raw", reactApplicationContext.packageName)
56
+ if (resId != 0) resources.openRawResource(resId) else File(uri).inputStream()
57
+ }
58
+ }
59
+
60
+ companion object {
61
+ const val NAME = "NanoIconsFontLoader"
62
+ }
63
+ }
@@ -3,6 +3,7 @@ package com.nanoicons
3
3
  import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
5
5
  import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
6
7
  import com.facebook.react.module.model.ReactModuleInfoProvider
7
8
  import com.facebook.react.uimanager.ViewManager
8
9
 
@@ -15,8 +16,24 @@ class NanoIconsPackage : BaseReactPackage() {
15
16
  override fun getModule(
16
17
  name: String,
17
18
  reactContext: ReactApplicationContext
18
- ): NativeModule? = null
19
+ ): NativeModule? =
20
+ when (name) {
21
+ NanoIconsFontLoaderModule.NAME -> NanoIconsFontLoaderModule(reactContext)
22
+ else -> null
23
+ }
19
24
 
20
25
  override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
21
- ReactModuleInfoProvider { emptyMap() }
26
+ ReactModuleInfoProvider {
27
+ mapOf(
28
+ NanoIconsFontLoaderModule.NAME to
29
+ ReactModuleInfo(
30
+ NanoIconsFontLoaderModule.NAME,
31
+ NanoIconsFontLoaderModule.NAME,
32
+ false, // canOverrideExistingModule
33
+ false, // needsEagerInit
34
+ false, // isCxxModule
35
+ true // isTurboModule
36
+ )
37
+ )
38
+ }
22
39
  }
@@ -1,4 +1,9 @@
1
1
  #import <React/RCTViewComponentView.h>
2
+ #import <Foundation/Foundation.h>
2
3
 
3
4
  @interface NanoIconView : RCTViewComponentView
4
5
  @end
6
+
7
+ // Clears cached CTFontRef entries for `family` so the next render fetches a
8
+ // fresh reference after an OTA font re-registration within the same process.
9
+ void NanoIconInvalidateFontCache(NSString *family);
@@ -24,22 +24,38 @@ using namespace facebook::react;
24
24
  }
25
25
  @end
26
26
 
27
- // Process-wide CTFontRef cache keyed by (fontFamily, fontSize).
28
- // Avoids 1000× CTFontCreateWithName for identical (family, size) combos.
27
+ // CTFontRef cache keyed by "family:size" — avoids CTFontCreateWithName per render.
28
+ // File-scope so NanoIconInvalidateFontCache can clear it on OTA font swap.
29
+ static NSMutableDictionary *sNanoIconFontCache;
30
+ static dispatch_once_t sNanoIconFontCacheOnce;
31
+
29
32
  static CTFontRef NanoIconGetCachedFont(NSString *family, CGFloat size) {
30
- static NSMutableDictionary *cache;
31
- static dispatch_once_t onceToken;
32
- dispatch_once(&onceToken, ^{ cache = [NSMutableDictionary new]; });
33
+ dispatch_once(&sNanoIconFontCacheOnce, ^{ sNanoIconFontCache = [NSMutableDictionary new]; });
33
34
 
34
35
  NSString *key = [NSString stringWithFormat:@"%@:%.1f", family, size];
35
- id existing = cache[key];
36
- if (existing) return (__bridge CTFontRef)existing;
36
+ id existing = sNanoIconFontCache[key];
37
+ if (existing) {
38
+ return (__bridge CTFontRef)existing;
39
+ }
37
40
 
38
41
  CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)family, size, NULL);
39
- if (font) cache[key] = (__bridge id)font;
42
+ if (font) sNanoIconFontCache[key] = (__bridge id)font;
40
43
  return font;
41
44
  }
42
45
 
46
+ // Clears cached entries for `family` after OTA font re-registration.
47
+ void NanoIconInvalidateFontCache(NSString *family) {
48
+ dispatch_async(dispatch_get_main_queue(), ^{
49
+ if (!sNanoIconFontCache) return;
50
+ NSString *prefix = [family stringByAppendingString:@":"];
51
+ NSArray<NSString *> *keys = [sNanoIconFontCache.allKeys
52
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *k, id _) {
53
+ return [k hasPrefix:prefix];
54
+ }]];
55
+ [sNanoIconFontCache removeObjectsForKeys:keys];
56
+ });
57
+ }
58
+
43
59
  @implementation NanoIconView {
44
60
  CTFontRef _font; // borrowed from static cache — do NOT CFRelease
45
61
  NSString *_fontFamily;
@@ -175,11 +191,17 @@ static CTFontRef NanoIconGetCachedFont(NSString *family, CGFloat size) {
175
191
  _drawingLayer = layer;
176
192
  }
177
193
 
178
- // Re-detect inline state when the view moves to a new parent.
194
+ // Reset inline state when the view moves to a new parent.
179
195
  - (void)didMoveToSuperview {
180
196
  [super didMoveToSuperview];
181
197
  _inlineDetected = NO;
198
+ _isInlineInText = NO;
199
+ _paragraphView = nil;
182
200
  _baselineOffsetValid = NO;
201
+ if (_drawingLayer) {
202
+ [_drawingLayer removeFromSuperlayer];
203
+ _drawingLayer = nil;
204
+ }
183
205
  }
184
206
 
185
207
  // Invalidate cached offset when size changes (text relayout).
@@ -339,6 +361,10 @@ static CTFontRef NanoIconGetCachedFont(NSString *family, CGFloat size) {
339
361
 
340
362
  // Map Unicode codepoints to font glyph IDs, handling surrogate pairs for codepoints > 0xFFFF.
341
363
  if (fontChanged || oldViewProps.codepoints != newViewProps.codepoints) {
364
+ // Re-fetch on codepoint change — cache may have been cleared by OTA font swap.
365
+ if (!fontChanged && _fontFamily) {
366
+ _font = NanoIconGetCachedFont(_fontFamily, _fontSize);
367
+ }
342
368
  const auto &codepoints = newViewProps.codepoints;
343
369
  _glyphs.resize(codepoints.size());
344
370
  for (size_t i = 0; i < codepoints.size(); i++) {
@@ -0,0 +1,11 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import <RNNanoIconsSpec/RNNanoIconsSpec.h>
3
+
4
+ @interface NanoIconsFontLoader : NSObject <NativeNanoIconsFontLoaderSpec>
5
+ @end
6
+ #else
7
+ #import <React/RCTBridgeModule.h>
8
+
9
+ @interface NanoIconsFontLoader : NSObject <RCTBridgeModule>
10
+ @end
11
+ #endif
@@ -0,0 +1,110 @@
1
+ #import "NanoIconsFontLoader.h"
2
+ #import "NanoIconView.h"
3
+ #import <CoreText/CoreText.h>
4
+
5
+ @implementation NanoIconsFontLoader
6
+
7
+ RCT_EXPORT_MODULE()
8
+
9
+ // `family` must match the TTF's PostScript/full name — CTFontManager ignores it otherwise.
10
+ RCT_EXPORT_METHOD(registerFont:(NSString *)family
11
+ uri:(NSString *)uri
12
+ resolve:(RCTPromiseResolveBlock)resolve
13
+ reject:(RCTPromiseRejectBlock)reject)
14
+ {
15
+ NSURL *url = [uri hasPrefix:@"/"] ? [NSURL fileURLWithPath:uri]
16
+ : [NSURL URLWithString:uri];
17
+ if (!url) {
18
+ reject(@"E_NANOICONS_FONT_REGISTER",
19
+ [NSString stringWithFormat:@"Invalid font uri: %@", uri], nil);
20
+ return;
21
+ }
22
+
23
+ NSData *data = [NSData dataWithContentsOfURL:url];
24
+ if (!data) {
25
+ reject(@"E_NANOICONS_FONT_REGISTER",
26
+ [NSString stringWithFormat:@"Could not read font at %@", uri], nil);
27
+ return;
28
+ }
29
+
30
+ CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
31
+ CGFontRef cgFont = CGFontCreateWithDataProvider(provider);
32
+ CGDataProviderRelease(provider);
33
+ if (!cgFont) {
34
+ reject(@"E_NANOICONS_FONT_REGISTER", @"Invalid font data", nil);
35
+ return;
36
+ }
37
+
38
+ NSString *postScriptName = (__bridge_transfer NSString *)CGFontCopyPostScriptName(cgFont);
39
+ NSString *fullName = (__bridge_transfer NSString *)CGFontCopyFullName(cgFont);
40
+
41
+ BOOL nameMatches =
42
+ [postScriptName isEqualToString:family] || [fullName isEqualToString:family];
43
+ if (!nameMatches) {
44
+ CGFontRelease(cgFont);
45
+ reject(
46
+ @"E_NANOICONS_FONT_REGISTER",
47
+ [NSString stringWithFormat:
48
+ @"Font name \"%@\" does not match family \"%@\". "
49
+ @"On iOS the TTF PostScript/full name must equal glyphMap.m.f.",
50
+ postScriptName ?: fullName, family],
51
+ nil);
52
+ return;
53
+ }
54
+
55
+ CFErrorRef error = NULL;
56
+ bool ok = CTFontManagerRegisterGraphicsFont(cgFont, &error);
57
+ // defer cgFont release — needed for re-registration if name conflict.
58
+
59
+ if (!ok && error) {
60
+ NSError *err = (__bridge_transfer NSError *)error;
61
+
62
+ if (err.code == kCTFontManagerErrorAlreadyRegistered ||
63
+ err.code == kCTFontManagerErrorDuplicatedName) {
64
+ // OTA reload: process still holds the old font. Swap it out.
65
+ CTFontRef existingCT = CTFontCreateWithName((__bridge CFStringRef)postScriptName, 10.0, NULL);
66
+ if (existingCT) {
67
+ CGFontRef existingCG = CTFontCopyGraphicsFont(existingCT, NULL);
68
+ CFRelease(existingCT);
69
+ if (existingCG) {
70
+ CFErrorRef unregErr = NULL;
71
+ CTFontManagerUnregisterGraphicsFont(existingCG, &unregErr);
72
+ CFRelease(existingCG);
73
+ if (unregErr) CFRelease(unregErr);
74
+ }
75
+ }
76
+
77
+ CFErrorRef regErr = NULL;
78
+ bool reok = CTFontManagerRegisterGraphicsFont(cgFont, &regErr);
79
+ CGFontRelease(cgFont);
80
+
81
+ if (!reok && regErr) {
82
+ NSError *reErr = (__bridge_transfer NSError *)regErr;
83
+ reject(@"E_NANOICONS_FONT_REGISTER", reErr.localizedDescription, reErr);
84
+ return;
85
+ }
86
+ if (regErr) CFRelease(regErr);
87
+
88
+ NanoIconInvalidateFontCache(postScriptName);
89
+ resolve(@(YES));
90
+ return;
91
+ }
92
+
93
+ CGFontRelease(cgFont);
94
+ reject(@"E_NANOICONS_FONT_REGISTER", err.localizedDescription, err);
95
+ return;
96
+ }
97
+
98
+ CGFontRelease(cgFont);
99
+ resolve(@(YES));
100
+ }
101
+
102
+ #ifdef RCT_NEW_ARCH_ENABLED
103
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
104
+ (const facebook::react::ObjCTurboModule::InitParams &)params
105
+ {
106
+ return std::make_shared<facebook::react::NativeNanoIconsFontLoaderSpecJSI>(params);
107
+ }
108
+ #endif
109
+
110
+ @end
@@ -12,11 +12,14 @@ export type IconSetConfig = {
12
12
  safeZone?: number;
13
13
  /** First Unicode codepoint for glyphs (default 0xe900). Hex string or number. */
14
14
  startUnicode?: number | string;
15
+ /** Linking type for the font (default 'static'). Static bundles the TTF, dynamic delivers it via OTA*/
16
+ linking?: 'static' | 'dynamic';
15
17
  };
16
18
  export type BuiltFont = {
17
19
  fontFamily: string;
18
20
  ttfPath: string;
19
21
  glyphmapPath: string;
22
+ linking: 'static' | 'dynamic';
20
23
  };
21
24
  /**
22
25
  * Build TTF + glyphmap for all icon sets using a single Pyodide/PathKit instance.
@@ -11,7 +11,7 @@ const fingerPrint_js_1 = require("../src/utils/fingerPrint.js");
11
11
  const DEFAULT_SAFE_ZONE = 1020;
12
12
  const DEFAULT_UPM = 1024;
13
13
  const DEFAULT_START_UNICODE = 0xe900;
14
- function shouldSkipGeneration(inputHash, outputDir, fontFamily, logger) {
14
+ function shouldSkipGeneration(inputHash, outputDir, fontFamily, linking, logger) {
15
15
  const ttfPath = path_1.default.join(outputDir, `${fontFamily}.ttf`);
16
16
  const glyphmapPath = path_1.default.join(outputDir, `${fontFamily}.glyphmap.json`);
17
17
  if (!fs_1.default.existsSync(outputDir) ||
@@ -21,7 +21,8 @@ function shouldSkipGeneration(inputHash, outputDir, fontFamily, logger) {
21
21
  }
22
22
  const glyphmap = JSON.parse(fs_1.default.readFileSync(glyphmapPath, 'utf8'));
23
23
  const storedHash = glyphmap?.m?.h;
24
- if (storedHash && storedHash === inputHash) {
24
+ const storedLinking = glyphmap?.m?.l === 'd' ? 'dynamic' : 'static';
25
+ if (storedHash && storedHash === inputHash && storedLinking === linking) {
25
26
  logger?.info(`${fontFamily}: SVG fingerprint unchanged, skipping build.`);
26
27
  return true;
27
28
  }
@@ -40,6 +41,7 @@ async function buildAllFonts(iconSets, projectRoot, options) {
40
41
  const set = iconSets[i];
41
42
  const inputDir = path_1.default.resolve(projectRoot, set.inputDir);
42
43
  const fontFamily = set.fontFamily ?? path_1.default.basename(inputDir);
44
+ const linking = set.linking ?? 'static';
43
45
  if (!fs_1.default.existsSync(inputDir)) {
44
46
  throw new Error(`[react-native-nano-icons] Input directory does not exist: ${inputDir} (from "${set.inputDir}")`);
45
47
  }
@@ -49,8 +51,8 @@ async function buildAllFonts(iconSets, projectRoot, options) {
49
51
  const ttfPath = path_1.default.join(outputDir, `${fontFamily}.ttf`);
50
52
  const glyphmapPath = path_1.default.join(outputDir, `${fontFamily}.glyphmap.json`);
51
53
  const inputHash = (0, fingerPrint_js_1.getFingerprintSync)(inputDir);
52
- if (shouldSkipGeneration(inputHash, outputDir, fontFamily, logger)) {
53
- results.push({ fontFamily, ttfPath, glyphmapPath });
54
+ if (shouldSkipGeneration(inputHash, outputDir, fontFamily, linking, logger)) {
55
+ results.push({ fontFamily, ttfPath, glyphmapPath, linking });
54
56
  continue;
55
57
  }
56
58
  if (fs_1.default.existsSync(ttfPath))
@@ -68,6 +70,7 @@ async function buildAllFonts(iconSets, projectRoot, options) {
68
70
  ? parseInt(set.startUnicode, 16)
69
71
  : set.startUnicode
70
72
  : DEFAULT_START_UNICODE,
73
+ linking,
71
74
  };
72
75
  logger?.start(`Building ${fontFamily} (${i + 1}/${iconSets.length})…`);
73
76
  const out = await (0, index_js_1.runPipeline)(config, { inputDir, outputDir, tempDir }, { logger, inputHash });
@@ -75,6 +78,7 @@ async function buildAllFonts(iconSets, projectRoot, options) {
75
78
  fontFamily,
76
79
  ttfPath: out.ttfPath,
77
80
  glyphmapPath: out.glyphmapPath,
81
+ linking,
78
82
  });
79
83
  }
80
84
  if (allSkipped && results.length > 0) {
@@ -7,3 +7,4 @@ export type NanoIconsConfig = {
7
7
  * Throws with a helpful message if the file is missing or malformed.
8
8
  */
9
9
  export declare function loadNanoIconsConfig(configRoot: string): NanoIconsConfig;
10
+ export declare function loadDynamicIconSets(configRoot: string): IconSetConfig[];
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.loadNanoIconsConfig = loadNanoIconsConfig;
7
+ exports.loadDynamicIconSets = loadDynamicIconSets;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  /**
@@ -24,3 +25,12 @@ function loadNanoIconsConfig(configRoot) {
24
25
  }
25
26
  return config;
26
27
  }
28
+ function loadDynamicIconSets(configRoot) {
29
+ const config = loadNanoIconsConfig(configRoot);
30
+ const dynamicSets = config.iconSets.filter((s) => s.linking === 'dynamic');
31
+ if (dynamicSets.length === 0) {
32
+ throw new Error(`[react-native-nano-icons] No icon sets with linking: "dynamic" found in .nanoicons.json.\n` +
33
+ `--dynamic only processes icon sets where linking is set to "dynamic".`);
34
+ }
35
+ return dynamicSets;
36
+ }
@@ -0,0 +1,8 @@
1
+ import type { IconSetConfig } from './build.js';
2
+ /**
3
+ * Load icon sets configured with linking: 'dynamic' from the Expo app config
4
+ * (app.json, app.config.js, or app.config.ts).
5
+ *
6
+ * Requires @expo/config to be installed (present in all Expo projects).
7
+ */
8
+ export declare function loadDynamicSetsFromAppConfig(projectRoot: string): IconSetConfig[];
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadDynamicSetsFromAppConfig = loadDynamicSetsFromAppConfig;
4
+ /**
5
+ * Load icon sets configured with linking: 'dynamic' from the Expo app config
6
+ * (app.json, app.config.js, or app.config.ts).
7
+ *
8
+ * Requires @expo/config to be installed (present in all Expo projects).
9
+ */
10
+ function loadDynamicSetsFromAppConfig(projectRoot) {
11
+ let getConfig;
12
+ try {
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
14
+ ({ getConfig } = require('@expo/config'));
15
+ }
16
+ catch {
17
+ throw new Error(`[react-native-nano-icons] @expo/config not found — required for --dynamic --app-config.\n` +
18
+ `It should already be present in Expo projects. If missing: yarn add @expo/config`);
19
+ }
20
+ const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true });
21
+ const plugins = Array.isArray(exp.plugins) ? exp.plugins : [];
22
+ const entry = plugins.find((p) => Array.isArray(p) && p[0] === 'react-native-nano-icons');
23
+ if (!entry) {
24
+ throw new Error(`[react-native-nano-icons] Plugin "react-native-nano-icons" not found in app config.\n` +
25
+ `Add it to your app.json or app.config.js/ts under the "plugins" key.`);
26
+ }
27
+ const [, options] = entry;
28
+ const dynamicSets = (options.iconSets ?? []).filter((s) => s.linking === 'dynamic');
29
+ if (dynamicSets.length === 0) {
30
+ throw new Error(`[react-native-nano-icons] No icon sets with linking: "dynamic" found.\n` +
31
+ `--dynamic --app-config only processes icon sets where linking is set to "dynamic".`);
32
+ }
33
+ return dynamicSets;
34
+ }
@@ -1,4 +1,5 @@
1
1
  export { buildAllFonts, type IconSetConfig, type BuiltFont } from './build.js';
2
2
  export { createOraLogger, createQuietLogger, detectExpoLogLevel, type NanoLogger, type LogLevel, } from './logger.js';
3
- export { loadNanoIconsConfig, type NanoIconsConfig } from './config.js';
3
+ export { loadNanoIconsConfig, loadDynamicIconSets, type NanoIconsConfig, } from './config.js';
4
+ export { loadDynamicSetsFromAppConfig } from './expoConfig.js';
4
5
  export { linkBare } from './link.js';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.linkBare = exports.loadNanoIconsConfig = exports.detectExpoLogLevel = exports.createQuietLogger = exports.createOraLogger = exports.buildAllFonts = void 0;
3
+ exports.linkBare = exports.loadDynamicSetsFromAppConfig = exports.loadDynamicIconSets = exports.loadNanoIconsConfig = exports.detectExpoLogLevel = exports.createQuietLogger = exports.createOraLogger = exports.buildAllFonts = void 0;
4
4
  var build_js_1 = require("./build.js");
5
5
  Object.defineProperty(exports, "buildAllFonts", { enumerable: true, get: function () { return build_js_1.buildAllFonts; } });
6
6
  var logger_js_1 = require("./logger.js");
@@ -9,5 +9,8 @@ Object.defineProperty(exports, "createQuietLogger", { enumerable: true, get: fun
9
9
  Object.defineProperty(exports, "detectExpoLogLevel", { enumerable: true, get: function () { return logger_js_1.detectExpoLogLevel; } });
10
10
  var config_js_1 = require("./config.js");
11
11
  Object.defineProperty(exports, "loadNanoIconsConfig", { enumerable: true, get: function () { return config_js_1.loadNanoIconsConfig; } });
12
+ Object.defineProperty(exports, "loadDynamicIconSets", { enumerable: true, get: function () { return config_js_1.loadDynamicIconSets; } });
13
+ var expoConfig_js_1 = require("./expoConfig.js");
14
+ Object.defineProperty(exports, "loadDynamicSetsFromAppConfig", { enumerable: true, get: function () { return expoConfig_js_1.loadDynamicSetsFromAppConfig; } });
12
15
  var link_js_1 = require("./link.js");
13
16
  Object.defineProperty(exports, "linkBare", { enumerable: true, get: function () { return link_js_1.linkBare; } });