@symbiote-native/font 0.1.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/angular/services/fonts.service/index.d.ts +8 -0
  6. package/build/angular/services/fonts.service/index.js +64 -0
  7. package/build/core/font-loader.d.ts +4 -0
  8. package/build/core/font-loader.js +30 -0
  9. package/build/core/font-utils.d.ts +14 -0
  10. package/build/core/font-utils.js +16 -0
  11. package/build/core/font.d.ts +11 -0
  12. package/build/core/font.js +94 -0
  13. package/build/core/index.d.ts +5 -0
  14. package/build/core/index.js +3 -0
  15. package/build/core/memory.d.ts +6 -0
  16. package/build/core/memory.js +30 -0
  17. package/build/core/native-modules.d.ts +24 -0
  18. package/build/core/native-modules.js +7 -0
  19. package/build/core/types.d.ts +18 -0
  20. package/build/core/types.js +9 -0
  21. package/build/react/hooks/use-fonts/index.d.ts +2 -0
  22. package/build/react/hooks/use-fonts/index.js +28 -0
  23. package/build/react/index.d.ts +2 -0
  24. package/build/react/index.js +3 -0
  25. package/build/solid/index.d.ts +2 -0
  26. package/build/solid/index.js +5 -0
  27. package/build/solid/primitives/create-fonts.d.ts +6 -0
  28. package/build/solid/primitives/create-fonts.js +12 -0
  29. package/build/svelte/index.d.ts +2 -0
  30. package/build/svelte/index.js +5 -0
  31. package/build/svelte/runes/use-fonts.svelte.d.ts +5 -0
  32. package/build/svelte/runes/use-fonts.svelte.js +24 -0
  33. package/build/vue/composables/use-fonts/index.d.ts +6 -0
  34. package/build/vue/composables/use-fonts/index.js +18 -0
  35. package/build/vue/index.d.ts +2 -0
  36. package/build/vue/index.js +3 -0
  37. package/build-ngc/angular/index.d.ts +2 -0
  38. package/build-ngc/angular/index.js +5 -0
  39. package/build-ngc/angular/index.js.map +1 -0
  40. package/build-ngc/angular/services/fonts.service/index.d.ts +11 -0
  41. package/build-ngc/angular/services/fonts.service/index.js +24 -0
  42. package/build-ngc/angular/services/fonts.service/index.js.map +1 -0
  43. package/build-ngc/core/font-loader.d.ts +4 -0
  44. package/build-ngc/core/font-loader.js +31 -0
  45. package/build-ngc/core/font-loader.js.map +1 -0
  46. package/build-ngc/core/font-utils.d.ts +14 -0
  47. package/build-ngc/core/font-utils.js +17 -0
  48. package/build-ngc/core/font-utils.js.map +1 -0
  49. package/build-ngc/core/font.d.ts +11 -0
  50. package/build-ngc/core/font.js +95 -0
  51. package/build-ngc/core/font.js.map +1 -0
  52. package/build-ngc/core/index.d.ts +5 -0
  53. package/build-ngc/core/index.js +4 -0
  54. package/build-ngc/core/index.js.map +1 -0
  55. package/build-ngc/core/memory.d.ts +6 -0
  56. package/build-ngc/core/memory.js +31 -0
  57. package/build-ngc/core/memory.js.map +1 -0
  58. package/build-ngc/core/native-modules.d.ts +24 -0
  59. package/build-ngc/core/native-modules.js +8 -0
  60. package/build-ngc/core/native-modules.js.map +1 -0
  61. package/build-ngc/core/types.d.ts +18 -0
  62. package/build-ngc/core/types.js +10 -0
  63. package/build-ngc/core/types.js.map +1 -0
  64. package/native-link.json +17 -0
  65. package/package.json +151 -0
  66. package/src/angular/index.ts +4 -0
  67. package/src/angular/services/fonts.service/index.ts +21 -0
  68. package/src/core/font-loader.ts +44 -0
  69. package/src/core/font-utils.ts +39 -0
  70. package/src/core/font.ts +150 -0
  71. package/src/core/index.ts +18 -0
  72. package/src/core/memory.ts +39 -0
  73. package/src/core/native-modules.ts +45 -0
  74. package/src/core/types.ts +25 -0
  75. package/src/react/hooks/use-fonts/index.ts +36 -0
  76. package/src/react/index.ts +4 -0
  77. package/src/solid/index.ts +6 -0
  78. package/src/solid/primitives/create-fonts.ts +18 -0
  79. package/src/svelte/index.ts +6 -0
  80. package/src/svelte/runes/use-fonts.svelte.ts +30 -0
  81. package/src/svelte/svelte-ambient.d.ts +3 -0
  82. package/src/vue/composables/use-fonts/index.ts +24 -0
  83. package/src/vue/index.ts +4 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
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.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # @symbiote-native/font
2
+
3
+ Use a custom font without touching the native projects: give a name and a font file, wait for the
4
+ load, then reference the name in `fontFamily`. One API for every [SymbioteNative](../../README.md)
5
+ adapter (React, Vue, Svelte, Solid and Angular).
6
+
7
+ It wraps [`expo-font`](https://github.com/expo/expo/tree/main/packages/expo-font). A font source is
8
+ a string, URI, `require()` module id, or an [`@symbiote-native/asset`](../asset) `Asset`. The
9
+ `renderToImageAsync` text-to-image utility (iOS + Android) comes with it. Built the same way as
10
+ [`@symbiote-native/network`](../network), an `expo-modules-core`-based wrapper (see the
11
+ `symbiote-expo-native-module` project skill for the full mechanism).
12
+
13
+ ## Install
14
+
15
+ **New app:**
16
+
17
+ ```bash
18
+ npx @symbiote-native/cli new my-app --font
19
+ ```
20
+
21
+ **Existing SymbioteNative app:**
22
+
23
+ ```bash
24
+ npx @symbiote-native/cli add --font
25
+ ```
26
+
27
+ Either way: installs `@symbiote-native/font` and wires the native autolinking automatically - see
28
+ [`@symbiote-native/cli`](../cli).
29
+
30
+ <details>
31
+ <summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
32
+
33
+ ```bash
34
+ npm install @symbiote-native/font
35
+ ```
36
+
37
+ `expo-font`, `expo-modules-core`, and `@symbiote-native/asset` come along as regular dependencies,
38
+ pinned to exact versions - never install `expo-font` yourself, and never add the `expo` meta-package
39
+ to this project.
40
+
41
+ ### Required one-time step: native autolinking wiring
42
+
43
+ Same one-time step as every other `expo-modules-core` package this project ships - see
44
+ [`@symbiote-native/network`'s README](../network/README.md#required-one-time-step-native-autolinking-wiring)
45
+ and the `symbiote-expo-native-module` project skill. `native-link.json` registers two Android
46
+ modules (`FontLoaderModule` → `ExpoFontLoader`, `FontUtilsModule` → `ExpoFontUtils`); iOS needs no
47
+ manifest entry, both autolink via `use_expo_modules!`.
48
+
49
+ No platform permission string is needed - font loading/rendering reads no protected system state
50
+ on either platform.
51
+
52
+ </details>
53
+
54
+ ## Shape
55
+
56
+ ```
57
+ src/core/ font.ts - isLoaded/getLoadedFonts/isLoading/isFontMapLoaded/loadAsync/
58
+ unloadAllAsync/unloadAsync. font-loader.ts resolves a FontSource to an
59
+ @symbiote-native/asset Asset. font-utils.ts - renderToImageAsync.
60
+ memory.ts - the loaded-fonts cache. native-modules.ts resolves
61
+ ExpoFontLoader/ExpoFontUtils through expo-modules-core's
62
+ requireNativeModule/requireOptionalNativeModule.
63
+ src/react/hooks/ @symbiote-native/font/react - useFonts
64
+ src/vue/composables/ @symbiote-native/font/vue - useFonts (same name)
65
+ src/svelte/runes/ @symbiote-native/font/svelte - useFonts (same name)
66
+ src/solid/primitives/ @symbiote-native/font/solid - createFonts (Solid reserves `use*` for
67
+ consuming existing state)
68
+ src/angular/services/ @symbiote-native/font/angular - FontsService (`.connect()` returns a
69
+ Signal pair)
70
+ ```
71
+
72
+ Each adapter's hook/composable/rune/primitive/service is a thin lifecycle wrapper (seed
73
+ synchronously from `isFontMapLoaded`, call `loadAsync` once on mount, never reload on a changed
74
+ font map) over the same `core` functions.
75
+
76
+ ## Use it
77
+
78
+ ```tsx
79
+ // React
80
+ import { loadAsync } from '@symbiote-native/font';
81
+ import { useFonts } from '@symbiote-native/font/react';
82
+
83
+ function App() {
84
+ const [loaded, error] = useFonts({
85
+ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
86
+ });
87
+
88
+ if (!loaded) return null;
89
+ return <text style={{ fontFamily: 'Inter-Regular' }}>Hello</text>;
90
+ }
91
+ ```
92
+
93
+ ```vue
94
+ <!-- Vue -->
95
+ <script setup lang="ts">
96
+ import { useFonts } from '@symbiote-native/font/vue';
97
+
98
+ const { loaded, error } = useFonts({
99
+ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
100
+ });
101
+ </script>
102
+ <template>
103
+ <text v-if="loaded" style="font-family: Inter-Regular">Hello</text>
104
+ </template>
105
+ ```
106
+
107
+ ```svelte
108
+ <!-- Svelte -->
109
+ <script lang="ts">
110
+ import { useFonts } from '@symbiote-native/font/svelte';
111
+
112
+ const fonts = useFonts({
113
+ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
114
+ });
115
+ </script>
116
+
117
+ {#if fonts.loaded}
118
+ <text style="font-family: Inter-Regular">Hello</text>
119
+ {/if}
120
+ ```
121
+
122
+ ```ts
123
+ // Angular
124
+ import { Component, inject } from '@angular/core';
125
+ import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
126
+ import { FontsService } from '@symbiote-native/font/angular';
127
+
128
+ @Component({
129
+ standalone: true,
130
+ imports: [SYMBIOTE_ELEMENTS],
131
+ template: `@if (fonts.loaded()) {
132
+ <text style="font-family: Inter-Regular">Hello</text>
133
+ }`,
134
+ })
135
+ export class App {
136
+ readonly fonts = inject(FontsService).connect({
137
+ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
138
+ });
139
+ }
140
+ ```
141
+
142
+ ```tsx
143
+ // Solid - the accessor is CALLED; a Solid component body runs once.
144
+ import { createFonts } from '@symbiote-native/font/solid';
145
+
146
+ function App() {
147
+ const fonts = createFonts({
148
+ 'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
149
+ });
150
+
151
+ return fonts.loaded() ? (
152
+ <text style={{ 'font-family': 'Inter-Regular' }}>Hello</text>
153
+ ) : null;
154
+ }
155
+ ```
156
+
157
+ ## API
158
+
159
+ ```ts
160
+ isLoaded(fontFamily: string): boolean
161
+ getLoadedFonts(): string[]
162
+ isLoading(fontFamily: string): boolean
163
+ isFontMapLoaded(map: string | Record<string, FontSource>): boolean
164
+ loadAsync(fontFamilyOrFontMap: string | Record<string, FontSource>, source?: FontSource): Promise<void>
165
+ unloadAllAsync(): Promise<void> // always throws - see Notes
166
+ unloadAsync(fontFamilyOrFontMap: string | Record<string, UnloadFontOptions>, options?: UnloadFontOptions): Promise<void> // always throws - see Notes
167
+ renderToImageAsync(glyphs: string, options?: IRenderToImageOptions): Promise<IRenderToImageResult>
168
+ ```
169
+
170
+ Plus `FontDisplay` (enum, web-only effect, kept for type parity), `FontSource`, `FontResource`,
171
+ `UnloadFontOptions`, `UseFontsResult` - hand-ported from upstream's `Font.types.ts`.
172
+
173
+ ## Notes
174
+
175
+ - **The `fontFamily` in a style must match the map key exactly.** `'Inter-Regular'` in the map and
176
+ `fontFamily: 'Inter'` in the style silently falls back to the system font.
177
+ - **`unloadAsync`/`unloadAllAsync` always throw `UnavailabilityError` on native** - ported and
178
+ exported for API parity, but `ExpoFontLoader` has no unload method on either iOS or Android,
179
+ only on web. Matches upstream exactly; not a bug in this port.
180
+ - **The web/server branches are not ported** - `isLoaded`'s web fallback, `loadAsync`'s server
181
+ pre-render pass (`registerStaticFont`/`serverContext`), `FontHooks.ts`'s static-fonts SSR branch.
182
+ This repo has no web/SSR render target for any adapter.
183
+ - **`expo-font`'s config plugin (`withFonts`) is not ported** - declaratively bundles static font
184
+ files into the native project via `app.json`. This repo runs no Expo CLI/prebuild step at all
185
+ (see the root `CLAUDE.md`); bundle a static font the plain React Native way
186
+ (`android/app/src/main/assets/fonts/`, Xcode's "Copy Bundle Resources" + `UIAppFonts` in
187
+ `Info.plist`) or call `loadAsync` with a local `file://` URI at startup.
188
+
189
+ ## Common questions
190
+
191
+ - **The text uses the system font.** The `fontFamily` is not the exact key you loaded, you rendered
192
+ before `loaded`, or the file failed to load (check `error` and `getLoadedFonts()`).
193
+ - **Bold or italic.** Load each weight or style as its own family and set `fontFamily` to it; do
194
+ not rely on `fontWeight` for a custom family.
195
+ - **A natively bundled font works on Android, not iOS.** iOS finds a `UIAppFonts` font by its own
196
+ (full or PostScript) name, not the file name; a runtime-loaded font uses your map key.
197
+ - **Unloading.** `unloadAsync` throws on iOS and Android: the native loader has no unload.
198
+
199
+ Sources: [expo/expo#33673](https://github.com/expo/expo/issues/33673),
200
+ [expo/expo#22074](https://github.com/expo/expo/issues/22074),
201
+ [DEV: custom fonts on iOS](https://dev.to/aymericmartinache/the-mystery-of-fonts-on-ios-why-was-my-app-not-displaying-custom-fonts-3h8j).
202
+
203
+ ## Test it
204
+
205
+ No Fabric/Descriptor angle at all - every function here is a pure async-function surface plus a
206
+ small in-memory cache, never a view. Tests inject a fake native-module object in place of the real
207
+ `requireNativeModule` resolution (`src/core/{font,font-loader,font-utils,memory}.test.ts`,
208
+ `src/{react,vue,svelte,solid,angular}/**/*.test.{ts,tsx}`) - no `installFabric()`, no ViewConfig.
209
+
210
+ The `FontScreen` in the `examples/expo-*` apps carries the on-device verification.
@@ -0,0 +1,2 @@
1
+ export { FontsService } from './services/fonts.service';
2
+ export * from '../core';
@@ -0,0 +1,4 @@
1
+ // FontsService is the Angular-only lifecycle half; the loading/caching logic all lives in
2
+ // core, shared with React/Vue/Svelte/Solid.
3
+ export { FontsService } from './services/fonts.service/index.js';
4
+ export * from '../core/index.js';
@@ -0,0 +1,8 @@
1
+ import { type Signal } from '@angular/core';
2
+ import { type FontSource } from '../../../core';
3
+ export declare class FontsService {
4
+ connect(map: string | Record<string, FontSource>): {
5
+ loaded: Signal<boolean>;
6
+ error: Signal<Error | null>;
7
+ };
8
+ }
@@ -0,0 +1,64 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { Injectable, signal } from '@angular/core';
36
+ import { isFontMapLoaded, loadAsync } from '../../../core/index.js';
37
+ // Angular twin of React's useFonts hook and Vue's useFonts composable — connect(map) returns
38
+ // a Signal pair, e.g. `readonly fonts = inject(FontsService).connect({ Inter: require('...') })`.
39
+ let FontsService = (() => {
40
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
41
+ let _classDescriptor;
42
+ let _classExtraInitializers = [];
43
+ let _classThis;
44
+ var FontsService = class {
45
+ static { _classThis = this; }
46
+ static {
47
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
48
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
49
+ FontsService = _classThis = _classDescriptor.value;
50
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
51
+ __runInitializers(_classThis, _classExtraInitializers);
52
+ }
53
+ connect(map) {
54
+ const loaded = signal(isFontMapLoaded(map));
55
+ const error = signal(null);
56
+ loadAsync(map)
57
+ .then(() => loaded.set(true))
58
+ .catch((err) => error.set(err));
59
+ return { loaded: loaded.asReadonly(), error: error.asReadonly() };
60
+ }
61
+ };
62
+ return FontsService = _classThis;
63
+ })();
64
+ export { FontsService };
@@ -0,0 +1,4 @@
1
+ import { Asset } from '@symbiote-native/asset';
2
+ import type { FontResource, FontSource } from './types';
3
+ export declare function getAssetForSource(source: FontSource): Asset | FontResource;
4
+ export declare function loadSingleFontAsync(name: string, input: Asset | FontResource): Promise<void>;
@@ -0,0 +1,30 @@
1
+ // Ported from expo-font/src/FontLoader.ts (sdk-57), resolving through @symbiote-native/asset's
2
+ // Asset class (this repo's own expo-asset port) instead of importing expo-asset directly.
3
+ import { Asset } from '@symbiote-native/asset';
4
+ import { CodedError } from 'expo-modules-core';
5
+ import { expoFontLoader } from './native-modules.js';
6
+ export function getAssetForSource(source) {
7
+ if (source instanceof Asset) {
8
+ return source;
9
+ }
10
+ if (typeof source === 'string') {
11
+ return Asset.fromURI(source);
12
+ }
13
+ if (typeof source === 'number') {
14
+ return Asset.fromModule(source);
15
+ }
16
+ if (typeof source === 'object' && typeof source.uri !== 'undefined') {
17
+ return getAssetForSource(source.uri);
18
+ }
19
+ return source;
20
+ }
21
+ export async function loadSingleFontAsync(name, input) {
22
+ if (!(input instanceof Asset)) {
23
+ throw new CodedError('ERR_FONT_SOURCE', '`loadSingleFontAsync` expected a resource of type `Asset` from @symbiote-native/asset');
24
+ }
25
+ await input.downloadAsync();
26
+ if (!input.downloaded || !input.localUri) {
27
+ throw new CodedError('ERR_DOWNLOAD', `Failed to download asset for font "${name}"`);
28
+ }
29
+ await expoFontLoader.loadAsync(name, input.localUri);
30
+ }
@@ -0,0 +1,14 @@
1
+ export type IRenderToImageOptions = {
2
+ fontFamily?: string;
3
+ size?: number;
4
+ color?: string;
5
+ lineHeight?: number;
6
+ };
7
+ export type IRenderToImageResult = {
8
+ uri: string;
9
+ width: number;
10
+ height: number;
11
+ scale: number;
12
+ };
13
+ /** @platform android @platform ios */
14
+ export declare function renderToImageAsync(glyphs: string, options?: IRenderToImageOptions): Promise<IRenderToImageResult>;
@@ -0,0 +1,16 @@
1
+ // Ported from expo-font/src/FontUtils.ts (sdk-57).
2
+ import { UnavailabilityError } from 'expo-modules-core';
3
+ import { processColor } from 'react-native';
4
+ import { expoFontUtils } from './native-modules.js';
5
+ /** @platform android @platform ios */
6
+ export async function renderToImageAsync(glyphs, options) {
7
+ if (!expoFontUtils) {
8
+ throw new UnavailabilityError('expo-font', 'ExpoFontUtils.renderToImageAsync');
9
+ }
10
+ return expoFontUtils.renderToImageAsync(glyphs, {
11
+ ...options,
12
+ color: options?.color
13
+ ? (processColor(options.color) ?? undefined)
14
+ : undefined,
15
+ });
16
+ }
@@ -0,0 +1,11 @@
1
+ import type { FontSource, UnloadFontOptions } from './types';
2
+ export declare function isLoaded(fontFamily: string): boolean;
3
+ export declare function getLoadedFonts(): string[];
4
+ export declare function isLoading(fontFamily: string): boolean;
5
+ /** Shared seed check for every adapter's `useFonts`/`createFonts` lifecycle wrapper. */
6
+ export declare function isFontMapLoaded(map: string | Record<string, FontSource>): boolean;
7
+ export declare function loadAsync(fontFamilyOrFontMap: string | Record<string, FontSource>, source?: FontSource): Promise<void>;
8
+ /** Always throws on native — ExpoFontLoader has no unload method there, web-only upstream. */
9
+ export declare function unloadAllAsync(): Promise<void>;
10
+ /** Always throws on native — ExpoFontLoader has no unload method there, web-only upstream. */
11
+ export declare function unloadAsync(fontFamilyOrFontMap: string | Record<string, UnloadFontOptions>, options?: UnloadFontOptions): Promise<void>;
@@ -0,0 +1,94 @@
1
+ // Ported from expo-font/src/Font.ts (sdk-57), native-only — drops the web/server branches
2
+ // (isLoaded's web fallback, loadAsync's server pre-render pass, registerStaticFont).
3
+ import { CodedError, UnavailabilityError } from 'expo-modules-core';
4
+ import { getAssetForSource, loadSingleFontAsync } from './font-loader.js';
5
+ import { isLoadedNative, loadPromises, markLoaded, purgeCache, purgeFontFamilyFromCache, } from './memory.js';
6
+ import { expoFontLoader } from './native-modules.js';
7
+ export function isLoaded(fontFamily) {
8
+ return isLoadedNative(fontFamily);
9
+ }
10
+ export function getLoadedFonts() {
11
+ return expoFontLoader.getLoadedFonts();
12
+ }
13
+ export function isLoading(fontFamily) {
14
+ return fontFamily in loadPromises;
15
+ }
16
+ /** Shared seed check for every adapter's `useFonts`/`createFonts` lifecycle wrapper. */
17
+ export function isFontMapLoaded(map) {
18
+ if (typeof map === 'string') {
19
+ return isLoaded(map);
20
+ }
21
+ return Object.keys(map).every(fontFamily => isLoaded(fontFamily));
22
+ }
23
+ export function loadAsync(fontFamilyOrFontMap, source) {
24
+ if (typeof fontFamilyOrFontMap === 'object') {
25
+ if (source) {
26
+ return Promise.reject(new CodedError('ERR_FONT_API', `No fontFamily can be used for the provided source: ${String(source)}. The second argument of loadAsync() can only be used with a string value as the first argument.`));
27
+ }
28
+ const fontMap = fontFamilyOrFontMap;
29
+ const names = Object.keys(fontMap);
30
+ return Promise.all(names.map(name => loadFontInNamespaceAsync(name, fontMap[name]))).then(() => { });
31
+ }
32
+ return loadFontInNamespaceAsync(fontFamilyOrFontMap, source);
33
+ }
34
+ async function loadFontInNamespaceAsync(fontFamily, source) {
35
+ if (!source) {
36
+ throw new CodedError('ERR_FONT_SOURCE', `Cannot load null or undefined font source for fontFamily "${fontFamily}".`);
37
+ }
38
+ if (isLoaded(fontFamily)) {
39
+ return;
40
+ }
41
+ if (Object.hasOwn(loadPromises, fontFamily)) {
42
+ return loadPromises[fontFamily];
43
+ }
44
+ const asset = getAssetForSource(source);
45
+ loadPromises[fontFamily] = (async () => {
46
+ try {
47
+ await loadSingleFontAsync(fontFamily, asset);
48
+ markLoaded(fontFamily);
49
+ }
50
+ finally {
51
+ delete loadPromises[fontFamily];
52
+ }
53
+ })();
54
+ await loadPromises[fontFamily];
55
+ }
56
+ /** Always throws on native — ExpoFontLoader has no unload method there, web-only upstream. */
57
+ export async function unloadAllAsync() {
58
+ if (!expoFontLoader.unloadAllAsync) {
59
+ throw new UnavailabilityError('expo-font', 'unloadAllAsync');
60
+ }
61
+ if (Object.keys(loadPromises).length) {
62
+ throw new CodedError('ERR_UNLOAD', `Cannot unload fonts while they're still loading: ${Object.keys(loadPromises).join(', ')}`);
63
+ }
64
+ purgeCache();
65
+ await expoFontLoader.unloadAllAsync();
66
+ }
67
+ /** Always throws on native — ExpoFontLoader has no unload method there, web-only upstream. */
68
+ export async function unloadAsync(fontFamilyOrFontMap, options) {
69
+ if (!expoFontLoader.unloadAsync) {
70
+ throw new UnavailabilityError('expo-font', 'unloadAsync');
71
+ }
72
+ if (typeof fontFamilyOrFontMap === 'object') {
73
+ if (options) {
74
+ throw new CodedError('ERR_FONT_API', `No fontFamily can be used for the provided options: ${String(options)}. The second argument of unloadAsync() can only be used with a string value as the first argument.`);
75
+ }
76
+ const names = Object.keys(fontFamilyOrFontMap);
77
+ await Promise.all(names.map(name => unloadFontInNamespaceAsync(name, fontFamilyOrFontMap[name])));
78
+ return;
79
+ }
80
+ return unloadFontInNamespaceAsync(fontFamilyOrFontMap, options);
81
+ }
82
+ async function unloadFontInNamespaceAsync(fontFamily, options) {
83
+ if (!isLoaded(fontFamily)) {
84
+ return;
85
+ }
86
+ purgeFontFamilyFromCache(fontFamily);
87
+ if (!fontFamily) {
88
+ throw new CodedError('ERR_FONT_FAMILY', 'Cannot unload an empty name');
89
+ }
90
+ if (!expoFontLoader.unloadAsync) {
91
+ throw new UnavailabilityError('expo-font', 'unloadAsync');
92
+ }
93
+ await expoFontLoader.unloadAsync(fontFamily, options);
94
+ }
@@ -0,0 +1,5 @@
1
+ export { isLoaded, getLoadedFonts, isLoading, isFontMapLoaded, loadAsync, unloadAllAsync, unloadAsync, } from './font';
2
+ export { renderToImageAsync } from './font-utils';
3
+ export type { IRenderToImageOptions, IRenderToImageResult } from './font-utils';
4
+ export { FontDisplay } from './types';
5
+ export type { FontResource, FontSource, UnloadFontOptions, UseFontsResult, } from './types';
@@ -0,0 +1,3 @@
1
+ export { isLoaded, getLoadedFonts, isLoading, isFontMapLoaded, loadAsync, unloadAllAsync, unloadAsync, } from './font.js';
2
+ export { renderToImageAsync } from './font-utils.js';
3
+ export { FontDisplay } from './types.js';
@@ -0,0 +1,6 @@
1
+ export declare const loadPromises: Record<string, Promise<void>>;
2
+ export declare function markLoaded(fontFamily: string): void;
3
+ export declare function isLoadedInCache(fontFamily: string): boolean;
4
+ export declare function isLoadedNative(fontFamily: string): boolean;
5
+ export declare function purgeFontFamilyFromCache(fontFamily: string): void;
6
+ export declare function purgeCache(): void;
@@ -0,0 +1,30 @@
1
+ // Ported verbatim from expo-font/src/memory.ts (sdk-57) — no web/server branch to drop, this
2
+ // module was already native-agnostic.
3
+ import { expoFontLoader } from './native-modules.js';
4
+ export const loadPromises = {};
5
+ let cache = {};
6
+ export function markLoaded(fontFamily) {
7
+ cache[fontFamily] = true;
8
+ }
9
+ export function isLoadedInCache(fontFamily) {
10
+ return fontFamily in cache;
11
+ }
12
+ export function isLoadedNative(fontFamily) {
13
+ if (isLoadedInCache(fontFamily)) {
14
+ return true;
15
+ }
16
+ const loadedNativeFonts = expoFontLoader.getLoadedFonts();
17
+ if (!loadedNativeFonts?.length) {
18
+ return false;
19
+ }
20
+ loadedNativeFonts.forEach(font => {
21
+ cache[font] = true;
22
+ });
23
+ return fontFamily in cache;
24
+ }
25
+ export function purgeFontFamilyFromCache(fontFamily) {
26
+ delete cache[fontFamily];
27
+ }
28
+ export function purgeCache() {
29
+ cache = {};
30
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProcessedColorValue } from 'react-native';
2
+ import type { UnloadFontOptions } from './types';
3
+ export type INativeFontLoaderModule = {
4
+ getLoadedFonts(): string[];
5
+ loadAsync(fontFamilyName: string, localUri: string): Promise<void>;
6
+ unloadAllAsync?(): Promise<void>;
7
+ unloadAsync?(fontFamilyName: string, options?: UnloadFontOptions): Promise<void>;
8
+ };
9
+ export type INativeRenderToImageOptions = {
10
+ fontFamily?: string;
11
+ size?: number;
12
+ color?: ProcessedColorValue;
13
+ lineHeight?: number;
14
+ };
15
+ export type INativeFontUtilsModule = {
16
+ renderToImageAsync(glyphs: string, options?: INativeRenderToImageOptions): Promise<{
17
+ uri: string;
18
+ width: number;
19
+ height: number;
20
+ scale: number;
21
+ }>;
22
+ };
23
+ export declare const expoFontLoader: INativeFontLoaderModule;
24
+ export declare const expoFontUtils: INativeFontUtilsModule | null;
@@ -0,0 +1,7 @@
1
+ import { requireNativeModule, requireOptionalNativeModule, } from 'expo-modules-core';
2
+ const EXPO_FONT_LOADER_MODULE_NAME = 'ExpoFontLoader';
3
+ const EXPO_FONT_UTILS_MODULE_NAME = 'ExpoFontUtils';
4
+ export const expoFontLoader = requireNativeModule(EXPO_FONT_LOADER_MODULE_NAME);
5
+ // Present on both iOS and Android upstream — still optional (matches upstream's own
6
+ // requireOptionalNativeModule) since only web ships without it.
7
+ export const expoFontUtils = requireOptionalNativeModule(EXPO_FONT_UTILS_MODULE_NAME);
@@ -0,0 +1,18 @@
1
+ import type { Asset } from '@symbiote-native/asset';
2
+ /** @platform web — has no effect on native, kept for API-surface parity with upstream. */
3
+ export declare enum FontDisplay {
4
+ AUTO = "auto",
5
+ SWAP = "swap",
6
+ BLOCK = "block",
7
+ FALLBACK = "fallback",
8
+ OPTIONAL = "optional"
9
+ }
10
+ export type FontResource = {
11
+ uri?: string | number;
12
+ display?: FontDisplay;
13
+ default?: string;
14
+ testString?: string;
15
+ };
16
+ export type FontSource = string | number | Asset | FontResource;
17
+ export type UnloadFontOptions = Pick<FontResource, 'display'>;
18
+ export type UseFontsResult = readonly [loaded: boolean, error: Error | null];
@@ -0,0 +1,9 @@
1
+ /** @platform web — has no effect on native, kept for API-surface parity with upstream. */
2
+ export var FontDisplay;
3
+ (function (FontDisplay) {
4
+ FontDisplay["AUTO"] = "auto";
5
+ FontDisplay["SWAP"] = "swap";
6
+ FontDisplay["BLOCK"] = "block";
7
+ FontDisplay["FALLBACK"] = "fallback";
8
+ FontDisplay["OPTIONAL"] = "optional";
9
+ })(FontDisplay || (FontDisplay = {}));
@@ -0,0 +1,2 @@
1
+ import { type FontSource, type UseFontsResult } from '../../../core';
2
+ export declare function useFonts(map: string | Record<string, FontSource>): UseFontsResult;
@@ -0,0 +1,28 @@
1
+ // React lifecycle wiring over the framework-agnostic core (core/font.ts). Loads once on mount
2
+ // (upstream's own contract: a changed font map is not reloaded — see FontHooks.ts's
3
+ // useRuntimeFonts), seeded synchronously so an already-loaded font never flashes unloaded.
4
+ import { useEffect, useState } from 'react';
5
+ import { isFontMapLoaded, loadAsync, } from '../../../core/index.js';
6
+ export function useFonts(map) {
7
+ const [loaded, setLoaded] = useState(() => isFontMapLoaded(map));
8
+ const [error, setError] = useState(null);
9
+ useEffect(() => {
10
+ let isMounted = true;
11
+ loadAsync(map)
12
+ .then(() => {
13
+ if (isMounted)
14
+ setLoaded(true);
15
+ })
16
+ .catch((err) => {
17
+ if (isMounted)
18
+ setError(err);
19
+ });
20
+ return () => {
21
+ isMounted = false;
22
+ };
23
+ // Loads once on mount only — a later change to `map` is deliberately not reloaded,
24
+ // matching upstream's own useFonts contract.
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ }, []);
27
+ return [loaded, error];
28
+ }
@@ -0,0 +1,2 @@
1
+ export { useFonts } from './hooks/use-fonts';
2
+ export * from '../core';
@@ -0,0 +1,3 @@
1
+ // @symbiote-native/font/react: the React entry over the framework-agnostic core.
2
+ export { useFonts } from './hooks/use-fonts/index.js';
3
+ export * from '../core/index.js';
@@ -0,0 +1,2 @@
1
+ export { createFonts } from './primitives/create-fonts';
2
+ export * from '../core';