/ so "..." is untouched.
+ // - Repeat until stable: a single pass leaves ". ." from triple nesting.
// - Trim leading ". " at cell start
// - Remove orphan "-" after periods (upstream renders a bare dash for empty descriptions)
- const cellHtml = $cell
- .html()!
- .replace(/\.\s*\.\s*/g, '. ')
+ const blocks: string[] = [];
+ let cellHtml = $cell.html()!.replace(/<(code|pre)\b[^>]*>[\S\s]*?<\/\1>/gi, match => {
+ blocks.push(match);
+ return `%%MD_CODE_${blocks.length - 1}%%`;
+ });
+ while (/\.\s*\./.test(cellHtml)) {
+ cellHtml = cellHtml.replace(/\.\s*\./g, '. ');
+ }
+ cellHtml = cellHtml
.replace(/^\s*\.\s*/, '')
- .replace(/\.\s*-\s*$/, '.');
+ .replace(/\.\s*-\s*$/, '.')
+ .replace(/%%MD_CODE_(\d+)%%/g, (_, i) => blocks[Number(i)]);
$cell.html(cellHtml);
});
@@ -1027,6 +1040,10 @@ export function checkPage(markdown: string, pagePath?: string): string[] {
errors.push('Contains CSS class names in text');
}
+ if (markdown.includes('. .')) {
+ errors.push('Contains ". ." (corrupted ellipsis or doubled period)');
+ }
+
return errors.filter(error => !exemptions.some(ex => error.startsWith(ex)));
}
diff --git a/docs/ui/components/ComponentExample.tsx b/docs/ui/components/ComponentExample.tsx
new file mode 100644
index 00000000000000..89bfd1255f3c0f
--- /dev/null
+++ b/docs/ui/components/ComponentExample.tsx
@@ -0,0 +1,123 @@
+import { useTheme } from '@expo/styleguide';
+import { FileCode01Icon } from '@expo/styleguide-icons/outline/FileCode01Icon';
+import { PropsWithChildren, useEffect, useState } from 'react';
+
+import { cleanCopyValue, getCodeBlockDataFromChildren } from '~/common/code-utilities';
+import { prefersDarkTheme } from '~/common/window';
+import { usePageApiVersion } from '~/providers/page-api-version';
+import { LightboxImage } from '~/ui/components/ContentSpotlight/LightboxImage';
+import { Snippet } from '~/ui/components/Snippet/Snippet';
+import { SnippetContent } from '~/ui/components/Snippet/SnippetContent';
+import { SnippetHeader } from '~/ui/components/Snippet/SnippetHeader';
+import { CopyAction } from '~/ui/components/Snippet/actions/CopyAction';
+import { SettingsAction } from '~/ui/components/Snippet/actions/SettingsAction';
+
+type Props = PropsWithChildren<{
+ title: string;
+ src: string;
+ alt: string;
+ darkSrc?: string;
+}>;
+
+type SideButton = {
+ side: 'left' | 'right';
+ top: string;
+ height: string;
+};
+
+const DEVICE_FRAMES = {
+ ios: {
+ width: 1206,
+ height: 2622,
+ frameRadius: 30,
+ screenRadius: 24,
+ buttons: [
+ { side: 'left', top: '13%', height: '4%' },
+ { side: 'left', top: '21%', height: '7%' },
+ { side: 'left', top: '30%', height: '7%' },
+ { side: 'right', top: '24%', height: '11%' },
+ ] satisfies SideButton[],
+ },
+ android: {
+ width: 1080,
+ height: 2400,
+ frameRadius: 24,
+ screenRadius: 18,
+ buttons: [
+ { side: 'right', top: '17%', height: '6%' },
+ { side: 'right', top: '26%', height: '11%' },
+ ] satisfies SideButton[],
+ },
+};
+
+/**
+ * Shows a code example next to a screenshot of what it renders, in a single
+ * snippet card: a full-width header with actions, code on the left, and the
+ * image on the right, stacked on small screens. The screenshot switches
+ * between `src` and `darkSrc` based on the active theme.
+ *
+ * The screenshot sits inside a device bezel so it reads as a phone screen
+ * rather than a floating image. The platform comes from the file name, which
+ * every capture carries as `-ios-` or `-android-`, so pages need no extra prop.
+ */
+export function ComponentExample({ title, src, darkSrc, alt, children }: Props) {
+ const { themeName } = useTheme();
+ const context = usePageApiVersion();
+ const [isDark, setDark] = useState(false);
+
+ useEffect(() => {
+ if (themeName === 'auto') {
+ setDark(prefersDarkTheme());
+ } else {
+ setDark(themeName === 'dark');
+ }
+ }, [themeName]);
+
+ const { value } = getCodeBlockDataFromChildren(children);
+ const activeSrc = isDark && darkSrc ? darkSrc : src;
+ const device = src.includes('-android-') ? DEVICE_FRAMES.android : DEVICE_FRAMES.ios;
+
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ {device.buttons.map(button => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/@expo/cli/package.json b/packages/@expo/cli/package.json
index 21b0843e1382a4..16f65d1df1cdf1 100644
--- a/packages/@expo/cli/package.json
+++ b/packages/@expo/cli/package.json
@@ -79,7 +79,7 @@
"@expo/xcpretty": "^4.4.4",
"@react-native/dev-middleware": "0.86.0",
"accepts": "^1.3.8",
- "agent-cli-detector": "^0.1.2",
+ "agent-cli-detector": "0.1.6",
"arg": "^5.0.2",
"bplist-creator": "0.1.0",
"bplist-parser": "^0.3.1",
diff --git a/packages/@expo/fingerprint/CHANGELOG.md b/packages/@expo/fingerprint/CHANGELOG.md
index e9a7e4de28ae01..6eb56a532ce8b7 100644
--- a/packages/@expo/fingerprint/CHANGELOG.md
+++ b/packages/@expo/fingerprint/CHANGELOG.md
@@ -14,6 +14,7 @@
- Added a `package` source type that hashes a dependency by its `package.json` name and version. ([#47667](https://github.com/expo/expo/pull/47667) by [@kudo](https://github.com/kudo))
- Added fingerprint presets (`strict`, `balanced`, `relaxed`), selectable via `preset` in **fingerprint.config.js** or the `--preset` CLI flag. ([#47668](https://github.com/expo/expo/pull/47668) by [@kudo](https://github.com/kudo))
- Added `SourceSkips.EasJson` and `SourceSkips.Easignore` to exclude `eas.json` and `.easignore` from fingerprints. ([#48586](https://github.com/expo/expo/pull/48586) by [@kudo](https://github.com/kudo))
+- Hash the font file an `expo-font` Android family declares once for all of its definitions, as done with variable fonts. ([#48737](https://github.com/expo/expo/pull/48737) by [@vonovak](https://github.com/vonovak))
### 🐛 Bug fixes
diff --git a/packages/@expo/fingerprint/src/sourcer/Expo.ts b/packages/@expo/fingerprint/src/sourcer/Expo.ts
index c771a4fd0c9356..4713a45562ba5b 100644
--- a/packages/@expo/fingerprint/src/sourcer/Expo.ts
+++ b/packages/@expo/fingerprint/src/sourcer/Expo.ts
@@ -46,9 +46,11 @@ export async function getExpoConfigSourcesAsync(
'expo-splash-screen'
);
const fontPluginProps = getConfigPluginProps<{
- // Type mirrors FontProps from expo-font/plugin/src/withFonts.ts
+ // Type mirrors FontProps from expo-font/plugin/src/withFonts.ts and must stay in sync with it
fonts?: string[];
- android?: { fonts?: (string | { fontDefinitions: { path: string }[] })[] };
+ android?: {
+ fonts?: (string | { path?: string; fontDefinitions: { path?: string }[] })[];
+ };
ios?: { fonts?: string[] };
}>(expoConfig, 'expo-font');
@@ -58,7 +60,11 @@ export async function getExpoConfigSourcesAsync(
...(isIos ? (fontPluginProps?.ios?.fonts ?? []) : []),
...(isAndroid
? (fontPluginProps?.android?.fonts ?? []).flatMap((f) =>
- typeof f === 'string' ? [f] : (f.fontDefinitions ?? []).map((d) => d.path)
+ typeof f === 'string'
+ ? [f]
+ : // When a variable font file backs several definitions,
+ // a family may name the file path once instead of each definition repeating it
+ (f.fontDefinitions ?? []).map((d) => d.path ?? f.path)
)
: []),
diff --git a/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts b/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts
index f0e7ad8a0c5050..52921c0478d36b 100644
--- a/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts
+++ b/packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts
@@ -424,6 +424,8 @@ describe(getExpoConfigSourcesAsync, () => {
'assets/fonts/SF-Pro.ttf': 'sf pro data',
'assets/fonts/Roboto-Regular.ttf': 'roboto regular data',
'assets/fonts/Roboto-Bold.ttf': 'roboto bold data',
+ 'assets/fonts/RobotoFlex.ttf': 'roboto flex data',
+ 'assets/fonts/RobotoFlex-Italic.ttf': 'roboto flex italic data',
};
const pluginProps = {
fonts: ['./assets/fonts/SpaceMono-Regular.ttf'],
@@ -437,6 +439,17 @@ describe(getExpoConfigSourcesAsync, () => {
{ path: './assets/fonts/Roboto-Bold.ttf', weight: 700 },
],
},
+ {
+ // A family may name its file once instead of each definition repeating it.
+ fontFamily: 'Roboto Flex',
+ path: './assets/fonts/RobotoFlex.ttf',
+ fontDefinitions: [
+ { weight: 400 },
+ { weight: 700, axes: { wght: 650 } },
+ // A definition may still name a file of its own.
+ { path: './assets/fonts/RobotoFlex-Italic.ttf', weight: 400, style: 'italic' },
+ ],
+ },
],
},
};
@@ -446,11 +459,14 @@ describe(getExpoConfigSourcesAsync, () => {
expectFontSource(iosSources, 'assets/fonts/SF-Pro.ttf');
expectNoFontSource(iosSources, 'assets/fonts/Roboto-Regular.ttf');
expectNoFontSource(iosSources, 'assets/fonts/Roboto-Bold.ttf');
+ expectNoFontSource(iosSources, 'assets/fonts/RobotoFlex.ttf');
const androidSources = await getFontSources(files, pluginProps, 'android');
expectFontSource(androidSources, 'assets/fonts/SpaceMono-Regular.ttf');
expectFontSource(androidSources, 'assets/fonts/Roboto-Regular.ttf');
expectFontSource(androidSources, 'assets/fonts/Roboto-Bold.ttf');
+ expectFontSource(androidSources, 'assets/fonts/RobotoFlex.ttf');
+ expectFontSource(androidSources, 'assets/fonts/RobotoFlex-Italic.ttf');
expectNoFontSource(androidSources, 'assets/fonts/SF-Pro.ttf');
});
diff --git a/packages/expo-file-system/CHANGELOG.md b/packages/expo-file-system/CHANGELOG.md
index d87cf639ecf997..031572ef00d9ca 100644
--- a/packages/expo-file-system/CHANGELOG.md
+++ b/packages/expo-file-system/CHANGELOG.md
@@ -14,6 +14,7 @@
### 🐛 Bug fixes
+- [iOS] Fix wrong permissions for text() and bytes(). ([#42422](https://github.com/expo/expo/pull/42422)) by [@simoneldevig](https://github.com/simoneldevig))
- Fixed `copyAsync` on iOS copying the unedited original when a `ph://` asset has edits applied in Photos. ([#48248](https://github.com/expo/expo/pull/48248) by [@CoffeeFlux](https://github.com/CoffeeFlux))
- Fixed iOS file previews rejecting a new preview while the previous Quick Look dismissal animation is still finishing. ([#47947](https://github.com/expo/expo/pull/47947) by [@eliotgevers](https://github.com/eliotgevers))
- Added `./next` subpath to package `exports` field to resolve Metro bundler warning. ([#44793](https://github.com/expo/expo/pull/44793) by [@chang-in](https://github.com/chang-in))
diff --git a/packages/expo-file-system/ios/FileSystemFile.swift b/packages/expo-file-system/ios/FileSystemFile.swift
index 898b0fc4216e90..f9c1cd3da10883 100644
--- a/packages/expo-file-system/ios/FileSystemFile.swift
+++ b/packages/expo-file-system/ios/FileSystemFile.swift
@@ -157,13 +157,13 @@ internal final class FileSystemFile: FileSystemPath {
}
func text() throws -> String {
- return try withCorrectTypeAndScopedAccess(permission: .write) {
+ return try withCorrectTypeAndScopedAccess(permission: .read) {
return try String(contentsOf: url)
}
}
func bytes() throws -> Data {
- return try withCorrectTypeAndScopedAccess(permission: .write) {
+ return try withCorrectTypeAndScopedAccess(permission: .read) {
return try Data(contentsOf: url)
}
}
diff --git a/packages/expo-font/CHANGELOG.md b/packages/expo-font/CHANGELOG.md
index 7735e4b6871b48..84c252c48df425 100644
--- a/packages/expo-font/CHANGELOG.md
+++ b/packages/expo-font/CHANGELOG.md
@@ -8,7 +8,8 @@
### 🎉 New features
-- [android] Support variable fonts in the config plugin ([#48129](https://github.com/expo/expo/pull/48129) by [@L65FREAD](https://github.com/L65FREAD))
+- [android] Added an `axes` field to the config plugin's font definitions, so one variable font file can also back a slanted or condensed face through its `slnt`, `wdth` and other variation axes. ([#48621](https://github.com/expo/expo/pull/48621) by [@vonovak](https://github.com/vonovak))
+- [android] Support variable weight fonts in the config plugin ([#48129](https://github.com/expo/expo/pull/48129) by [@L65FREAD](https://github.com/L65FREAD))
- [ios] Apply `fontWeight` to variable fonts loaded with `useFonts`. ([#48432](https://github.com/expo/expo/pull/48432) by [@vonovak](https://github.com/vonovak))
### 🐛 Bug fixes
diff --git a/packages/expo-font/plugin/src/__tests__/withFontsAndroid-test.ts b/packages/expo-font/plugin/src/__tests__/withFontsAndroid-test.ts
index 2ad072a388ebc7..f730b5b5edaec3 100644
--- a/packages/expo-font/plugin/src/__tests__/withFontsAndroid-test.ts
+++ b/packages/expo-font/plugin/src/__tests__/withFontsAndroid-test.ts
@@ -1,7 +1,7 @@
import * as path from 'path';
import { toValidAndroidResourceName } from '../utils';
-import type { FontObject } from '../withFonts';
+import type { FontObject, FontVariationAxes } from '../withFonts';
import {
groupByFamily,
planFontCopies,
@@ -9,6 +9,10 @@ import {
generateFontManagerCalls,
assertNoConflictingDefinitions,
assertAndroidCanLoadFonts,
+ assertValidWeights,
+ assertValidAxes,
+ warnAboutUnknownAxisTags,
+ warnAboutUnslantedItalics,
} from '../withFontsAndroid';
const input = [
@@ -37,10 +41,34 @@ const input = [
fontDefinitions: [
{ path: './assets/fonts/Inter[wght].ttf', weight: 400 },
{ path: './assets/fonts/Inter[wght].ttf', weight: 700 },
+ // The same file slanted by its `slnt` axis, so one variable font also backs the oblique.
+ { path: './assets/fonts/Inter[wght].ttf', weight: 400, style: 'italic', axes: { slnt: -10 } },
+ ],
+ },
+ {
+ fontFamily: 'Roboto Flex',
+ // One variable file backs every definition, so the family names it once instead of each
+ // definition repeating it.
+ path: './assets/fonts/RobotoFlex.ttf',
+ fontDefinitions: [
+ { weight: 400 },
+ // A registered axis and one the font declares for itself. `slnt` is left undefined, the
+ // shape a conditional in app.config.ts writes for the axis it omits.
+ { weight: 700, axes: { wght: 650, slnt: undefined, wdth: 75, GRAD: -50 } },
+ // A definition may still name a file of its own.
+ { path: './assets/fonts/RobotoSerif_italic.ttf', weight: 400, style: 'italic' },
],
},
] as const satisfies FontObject[];
+const declaring = (axes: FontVariationAxes) =>
+ groupByFamily([
+ {
+ fontFamily: 'Roboto Flex',
+ fontDefinitions: [{ path: './RobotoFlex.ttf', weight: 400, axes }],
+ },
+ ]);
+
describe('groupByFamily', () => {
it('should group font definitions by font family', () => {
const expected = {
@@ -54,6 +82,22 @@ describe('groupByFamily', () => {
Inter: [
{ path: './assets/fonts/Inter[wght].ttf', weight: 400 },
{ path: './assets/fonts/Inter[wght].ttf', weight: 700 },
+ {
+ path: './assets/fonts/Inter[wght].ttf',
+ weight: 400,
+ style: 'italic',
+ axes: { slnt: -10 },
+ },
+ ],
+ // The family's `path` fills in every definition that declares none of its own.
+ 'Roboto Flex': [
+ { path: './assets/fonts/RobotoFlex.ttf', weight: 400 },
+ {
+ path: './assets/fonts/RobotoFlex.ttf',
+ weight: 700,
+ axes: { slnt: undefined, wght: 650, wdth: 75, GRAD: -50 },
+ },
+ { path: './assets/fonts/RobotoSerif_italic.ttf', weight: 400, style: 'italic' },
],
};
@@ -65,6 +109,12 @@ describe('groupByFamily', () => {
const result = groupByFamily([]);
expect(result).toEqual({});
});
+
+ it('should name the missing field when neither the family nor the definition holds a path', () => {
+ expect(() =>
+ groupByFamily([{ fontFamily: 'Roboto Flex', fontDefinitions: [{ weight: 700 }] }])
+ ).toThrow(/declares no "path"/);
+ });
});
describe('planFontCopies', () => {
@@ -133,6 +183,34 @@ describe('assertAndroidCanLoadFonts', () => {
});
});
+describe('assertValidWeights', () => {
+ const declaring = (weight: unknown) =>
+ groupByFamily([
+ {
+ fontFamily: 'Inter',
+ fontDefinitions: [{ path: './Inter[wght].ttf', weight: weight as number }],
+ },
+ ]);
+
+ it('should accept the weights Android resolves a family by', () => {
+ expect(() => assertValidWeights(groupByFamily(input))).not.toThrow();
+ expect(() => assertValidWeights(declaring(1))).not.toThrow();
+ expect(() => assertValidWeights(declaring(1000))).not.toThrow();
+ });
+
+ it('should reject a weight outside the range Android reads', () => {
+ expect(() => assertValidWeights(declaring(0))).toThrow(/1 to 1000/);
+ expect(() => assertValidWeights(declaring(1001))).toThrow(/1 to 1000/);
+ expect(() => assertValidWeights(declaring(400.5))).toThrow(/1 to 1000/);
+ expect(() => assertValidWeights(declaring('400'))).toThrow(/1 to 1000/);
+ });
+
+ it('should name the missing field when a definition carries no weight', () => {
+ expect(() => assertValidWeights(declaring(undefined))).toThrow(/declares no weight/);
+ expect(() => assertValidWeights(declaring(null))).toThrow(/declares no weight/);
+ });
+});
+
describe('assertNoConflictingDefinitions', () => {
it('should accept one variable font file backing several weights', () => {
// The shape the docs recommend for variable fonts, so it has to stay legal.
@@ -269,6 +347,50 @@ describe('getXmlSpecs', () => {
'app:fontVariationSettings': `'wght' 700`,
},
},
+ {
+ $: {
+ 'app:font': '@font/inter_wght_',
+ 'app:fontStyle': 'italic',
+ 'app:fontWeight': '400',
+ 'app:fontVariationSettings': `'wght' 400, 'slnt' -10`,
+ },
+ },
+ ],
+ },
+ },
+ },
+ {
+ // the family's `path` backing two weights, and one definition naming a file of its own
+ path: path.join(fontsDir, `xml_roboto_flex.xml`),
+ xml: {
+ 'font-family': {
+ $: { 'xmlns:app': 'http://schemas.android.com/apk/res-auto' },
+ font: [
+ {
+ $: {
+ 'app:font': '@font/roboto_flex',
+ 'app:fontStyle': 'normal',
+ 'app:fontWeight': '400',
+ 'app:fontVariationSettings': `'wght' 400`,
+ },
+ },
+ {
+ $: {
+ 'app:font': '@font/roboto_flex',
+ 'app:fontStyle': 'normal',
+ 'app:fontWeight': '700',
+ // `slnt` is undefined, so it is left out entirely.
+ 'app:fontVariationSettings': `'wght' 650, 'wdth' 75, 'GRAD' -50`,
+ },
+ },
+ {
+ $: {
+ 'app:font': '@font/roboto_serif_italic',
+ 'app:fontStyle': 'italic',
+ 'app:fontWeight': '400',
+ 'app:fontVariationSettings': `'wght' 400`,
+ },
+ },
],
},
},
@@ -285,6 +407,130 @@ describe('getXmlSpecs', () => {
});
});
+describe('assertValidAxes', () => {
+ it('should accept registered and custom axis tags', () => {
+ // `input` holds `slnt` left undefined, the registered `wdth`, and the font's own `GRAD`.
+ expect(() => assertValidAxes(groupByFamily(input))).not.toThrow();
+ });
+
+ it('should accept a tag padded to four characters', () => {
+ // The registry pads a tag holding fewer than four letters or digits with trailing spaces.
+ expect(() => assertValidAxes(declaring({ 'AB ': 1 }))).not.toThrow();
+ });
+
+ it('should reject an axis it cannot emit', () => {
+ expect(() => assertValidAxes(declaring({ slant: -10 }))).toThrow(/"slant".+four/s);
+ expect(() => assertValidAxes(declaring({ "a'b'": -10 }))).toThrow(/begins with a letter/);
+ expect(() => assertValidAxes(declaring({ ' ': -10 }))).toThrow(/begins with a letter/);
+ // A tag begins with a letter, so this one names no axis however the font is built.
+ expect(() => assertValidAxes(declaring({ '1abc': -10 }))).toThrow(/begins with a letter/);
+ // @ts-expect-error an axis takes a number
+ expect(() => assertValidAxes(declaring({ slnt: 'left' }))).toThrow();
+ });
+
+ it('should accept a registered tag spelled in the foundry namespace', () => {
+ // Uppercase names an axis a font declares for itself, so `SLNT` is not a misspelt `slnt`.
+ expect(() => assertValidAxes(declaring({ SLNT: -10, WDTH: 75 }))).not.toThrow();
+ expect(() => assertValidAxes(declaring({ WGHT: 650 }))).not.toThrow();
+ });
+
+ it('should reject a registered axis tag in a case that names no axis', () => {
+ expect(() => assertValidAxes(declaring({ Slnt: -10 }))).toThrow(/"Slnt".+"slnt"/s);
+ expect(() => assertValidAxes(declaring({ Wdth: 75 }))).toThrow(/"Wdth".+"wdth"/s);
+ expect(() => assertValidAxes(declaring({ Wght: 650 }))).toThrow(/"Wght".+"wght"/s);
+ });
+
+ it('should reject a§xes that hold no entries to read', () => {
+ expect(() => assertValidAxes(declaring('slnt' as unknown as FontVariationAxes))).toThrow(
+ /not an object/
+ );
+ expect(() => assertValidAxes(declaring([-10] as unknown as FontVariationAxes))).toThrow(
+ /not an object/
+ );
+ });
+});
+
+describe('warnAboutUnknownAxisTags', () => {
+ let warn: jest.SpyInstance;
+
+ beforeEach(() => {
+ warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ warn.mockRestore();
+ });
+
+ it('should warn about a foundry tag written in lowercase', () => {
+ // Roboto Flex declares `GRAD`, so `grad` names no axis and Android applies nothing.
+ warnAboutUnknownAxisTags(declaring({ grad: -50 }));
+
+ expect(warn).toHaveBeenCalledWith(expect.stringMatching(/"grad".+"GRAD"/s));
+ });
+
+ it('should warn about a registered tag written in the foundry namespace', () => {
+ // Legal, so `assertValidAxes` lets it through — but far more often `slnt` in the wrong case,
+ // and Android then applies nothing. Without this the mistake is silent.
+ warnAboutUnknownAxisTags(declaring({ SLNT: -10 }));
+
+ expect(warn).toHaveBeenCalledWith(expect.stringMatching(/"SLNT".+"slnt"/s));
+ });
+
+ it('should point at the registered tag for a foundry WGHT', () => {
+ warnAboutUnknownAxisTags(declaring({ WGHT: 650 }));
+
+ expect(warn).toHaveBeenCalledWith(expect.stringMatching(/"WGHT".+"wght"/s));
+ });
+
+ it('should stay quiet about tags a font can declare', () => {
+ warnAboutUnknownAxisTags(declaring({ slnt: -10, GRAD: -50, XTR2: 1, 'AB ': 1 }));
+ warnAboutUnknownAxisTags(groupByFamily(input));
+ // A tag that would warn stays quiet while it holds no value, so an axis left out of an
+ // app.config.ts conditional never reports.
+ warnAboutUnknownAxisTags(declaring({ grad: undefined }));
+
+ expect(warn).not.toHaveBeenCalled();
+ });
+});
+
+describe('warnAboutUnslantedItalics', () => {
+ let warn: jest.SpyInstance;
+
+ // One file backing both an upright and an italic face, the shape a variable font takes.
+ const backedBy = (italic: Partial) =>
+ groupByFamily([
+ {
+ fontFamily: 'Inter',
+ path: './Inter[wght].ttf',
+ fontDefinitions: [{ weight: 400 }, { weight: 400, style: 'italic', ...italic }],
+ },
+ ]);
+
+ beforeEach(() => {
+ warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ warn.mockRestore();
+ });
+
+ it('should warn when the file backing an italic face also backs an upright one without declaring slnt / ital', () => {
+ warnAboutUnslantedItalics(backedBy({}));
+
+ expect(warn).toHaveBeenCalledWith(expect.stringMatching(/"Inter".+will render upright/s));
+ });
+
+ it('should stay quiet when the italic definition slants the file or has own file path', () => {
+ warnAboutUnslantedItalics(backedBy({ axes: { slnt: -10 } }));
+ warnAboutUnslantedItalics(backedBy({ axes: { ital: 1 } }));
+ // A font may declare the slant as its own axis, which slants it just the same.
+ warnAboutUnslantedItalics(backedBy({ axes: { SLNT: -10 } }));
+ warnAboutUnslantedItalics(backedBy({ path: './Inter-Italic.ttf' }));
+
+ expect(warn).not.toHaveBeenCalled();
+ });
+});
+
describe('generateFontManagerCalls', () => {
it('supports kotlin and java', () => {
const resultKt = generateFontManagerCalls(groupByFamily(input), 'kt');
@@ -294,6 +540,7 @@ describe('generateFontManagerCalls', () => {
" ReactFontManager.getInstance().addCustomFont(this, "Source Serif 4", R.font.xml_source_serif_4)",
" ReactFontManager.getInstance().addCustomFont(this, "SpaceMono", R.font.xml_space_mono)",
" ReactFontManager.getInstance().addCustomFont(this, "Inter", R.font.xml_inter)",
+ " ReactFontManager.getInstance().addCustomFont(this, "Roboto Flex", R.font.xml_roboto_flex)",
]
`);
@@ -304,6 +551,7 @@ describe('generateFontManagerCalls', () => {
" ReactFontManager.getInstance().addCustomFont(this, "Source Serif 4", R.font.xml_source_serif_4);",
" ReactFontManager.getInstance().addCustomFont(this, "SpaceMono", R.font.xml_space_mono);",
" ReactFontManager.getInstance().addCustomFont(this, "Inter", R.font.xml_inter);",
+ " ReactFontManager.getInstance().addCustomFont(this, "Roboto Flex", R.font.xml_roboto_flex);",
]
`);
});
diff --git a/packages/expo-font/plugin/src/withFonts.ts b/packages/expo-font/plugin/src/withFonts.ts
index 202e4b465c7a44..57b7cb9b740225 100644
--- a/packages/expo-font/plugin/src/withFonts.ts
+++ b/packages/expo-font/plugin/src/withFonts.ts
@@ -5,17 +5,52 @@ import { withFontsIos } from './withFontsIos';
const pkg = require('../../package.json');
+/**
+ * The five axes the OpenType registry names, or any other four-character tag
+ * (a font may declare custom axes of its own, such as `GRAD`).
+ */
+export type FontVariationAxisTag = 'ital' | 'opsz' | 'slnt' | 'wdth' | 'wght' | (string & {});
+
+export type FontVariationAxes = Partial>;
+
+export type FontDefinition = {
+ /**
+ * For static fonts, each definition can point to a different font file.
+ */
+ path?: string | undefined;
+ weight: number;
+ style?: 'normal' | 'italic' | undefined;
+ /**
+ * The axes to instance a variable font at, such as `{ slnt: -10 }` for an oblique.
+ *
+ * `weight` and `style` pick which face the `fontWeight` and `fontStyle` JS props match; the axes
+ * here draw it. You may set them apart: `weight: 700` with `{ wght: 650 }`
+ * matches a request for bold and draws the file at 650. `wght` defaults to `weight`.
+ *
+ * `style` alone does not slant the glyphs, so an upright file declared `style: "italic"` renders
+ * upright until `slnt` or `ital` is set here.
+ *
+ * @platform android
+ */
+ axes?: FontVariationAxes | undefined;
+};
+
export type FontObject = {
fontFamily: string;
- fontDefinitions: {
- path: string;
- weight: number;
- style?: 'normal' | 'italic' | undefined;
- }[];
+ /**
+ * One variable font file can back several definitions; provide the file path once instead of repeating it per-definition.
+ *
+ * @platform android
+ */
+ path?: string | undefined;
+ fontDefinitions: FontDefinition[];
};
export type Font = string | FontObject;
+// NOTE(@vonovak): `@expo/fingerprint` reads these props to hash the font files this plugin embeds
+// (see `getExpoConfigSourcesAsync` in `packages/@expo/fingerprint/src/sourcer/Expo.ts`).
+// When a new way to declare a font file path is added here, teach the sourcer to read it too.
export type FontProps = {
/**
* An array of font file paths to link to the native project, relative to the project root.
diff --git a/packages/expo-font/plugin/src/withFontsAndroid.ts b/packages/expo-font/plugin/src/withFontsAndroid.ts
index bb86ffeab133df..c577c5955b778f 100644
--- a/packages/expo-font/plugin/src/withFontsAndroid.ts
+++ b/packages/expo-font/plugin/src/withFontsAndroid.ts
@@ -6,13 +6,14 @@ import {
XML,
CodeGenerator,
AndroidConfig,
+ WarningAggregator,
} from 'expo/config-plugins';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { resolveFontPaths, toValidAndroidResourceName } from './utils';
-import type { Font, FontObject } from './withFonts';
+import type { Font, FontDefinition, FontObject } from './withFonts';
const assetsFontsFir = 'app/src/main/assets/fonts';
const resourcesFontsDir = 'app/src/main/res/font';
@@ -30,17 +31,39 @@ export const withFontsAndroid: ConfigPlugin = (config, fonts) => {
return config;
};
-type GroupedFontObject = Record;
+/** A definition once the family's `path` has filled in the file it reads. */
+export type ResolvedFontDefinition = FontDefinition & { path: string };
+
+type GroupedFontObject = Record;
export function groupByFamily(array: FontObject[]): GroupedFontObject {
return array.reduce((result, item) => {
const keyValue = item['fontFamily'];
result[keyValue] ||= [];
- result[keyValue].push(...item.fontDefinitions);
+ result[keyValue].push(
+ ...item.fontDefinitions.map((it, index) => resolveFontFile(item, it, index))
+ );
return result;
}, {});
}
+function resolveFontFile(
+ family: FontObject,
+ definition: FontDefinition,
+ index: number
+): ResolvedFontDefinition {
+ const resolvedPath = definition.path ?? family.path;
+
+ if (!resolvedPath) {
+ throw new Error(
+ `Font family ${JSON.stringify(family.fontFamily)} declares no "path" at index ${index}. ` +
+ `Add "path" to the font definition, or next to the family.`
+ );
+ }
+
+ return { ...definition, path: resolvedPath };
+}
+
export function assertAndroidCanLoadFonts(fontsByFamily: GroupedFontObject) {
for (const [fontFamily, definitions] of Object.entries(fontsByFamily)) {
for (const definition of definitions) {
@@ -56,6 +79,37 @@ export function assertAndroidCanLoadFonts(fontsByFamily: GroupedFontObject) {
}
}
+const MIN_FONT_WEIGHT = 1;
+const MAX_FONT_WEIGHT = 1000;
+
+export function assertValidWeights(fontsByFamily: GroupedFontObject) {
+ for (const [fontFamily, definitions] of Object.entries(fontsByFamily)) {
+ for (const definition of definitions) {
+ const { weight } = definition;
+
+ if (weight === undefined || weight === null) {
+ throw new Error(
+ `Font family ${JSON.stringify(fontFamily)} declares no weight for ${definition.path}. ` +
+ `A weight is a whole number from ${MIN_FONT_WEIGHT} to ${MAX_FONT_WEIGHT}, such as 400 for regular or 700 for bold.`
+ );
+ }
+
+ if (
+ typeof weight === 'number' &&
+ Number.isInteger(weight) &&
+ weight >= MIN_FONT_WEIGHT &&
+ weight <= MAX_FONT_WEIGHT
+ ) {
+ continue;
+ }
+
+ throw new Error(
+ `Font family ${JSON.stringify(fontFamily)} declares weight ${JSON.stringify(weight)} for ${definition.path}. A weight is a whole number from ${MIN_FONT_WEIGHT} to ${MAX_FONT_WEIGHT}.`
+ );
+ }
+ }
+}
+
/**
* Throws when two definitions in the same family claim the same weight and style.
*
@@ -73,9 +127,9 @@ export function assertNoConflictingDefinitions(fontsByFamily: GroupedFontObject)
if (alreadyDeclaredBy) {
throw new Error(
- `Font family ${JSON.stringify(fontFamily)} declares more than one font for weight ${definition.weight} and style ${JSON.stringify(style)}: ${alreadyDeclaredBy} and ${definition.path}. ` +
- `Android resolves a font family by weight and style, so it cannot hold two fonts with the same pair — the app would crash on startup while registering the family. ` +
- `Remove the duplicate, or give each definition a weight or style of its own. One variable font file can back several weights, but each definition needs a different weight.`
+ `Font family ${JSON.stringify(fontFamily)} declares two fonts for weight ${definition.weight} and style ${JSON.stringify(style)}: ${alreadyDeclaredBy} and ${definition.path}. ` +
+ `Android matches a family on weight and style alone, so the app would crash on startup while registering it. ` +
+ `Give each definition a weight or style of its own — "axes" cannot tell them apart.`
);
}
@@ -84,10 +138,171 @@ export function assertNoConflictingDefinitions(fontsByFamily: GroupedFontObject)
}
}
+// https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
+const AXIS_TAG_LENGTH = 4;
+const AXIS_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9]* *$/;
+
+// Case splits the namespace: registered axes are lowercase, a font's own axes are uppercase, and a
+// tag in any other case names nothing. So `SLNT` is a font's own axis, not a misspelt `slnt`.
+const registeredAxisTags = ['ital', 'opsz', 'slnt', 'wdth', 'wght'];
+const FOUNDRY_AXIS_TAG_PATTERN = /^[A-Z][A-Z0-9]* *$/;
+
+function isFoundryAxisTag(tag: string) {
+ return FOUNDRY_AXIS_TAG_PATTERN.test(tag);
+}
+
+type DeclaredAxis = { tag: string; value: unknown; declares: string };
+
+/** Throws when a definition holds `axes` that cannot be read one axis at a time. */
+function collectDeclaredAxes(fontsByFamily: GroupedFontObject): DeclaredAxis[] {
+ return Object.entries(fontsByFamily).flatMap(([fontFamily, definitions]) =>
+ definitions.flatMap((definition) => {
+ const axes = definition.axes ?? {};
+
+ if (typeof axes !== 'object' || Array.isArray(axes)) {
+ throw new Error(
+ `Font family ${JSON.stringify(fontFamily)} declares "axes" for ${definition.path} as ${JSON.stringify(axes)}, which is not an object. ` +
+ `"axes" holds one entry per axis, such as { "slnt": -10 }.`
+ );
+ }
+
+ return Object.entries(axes).map(([tag, value]) => ({
+ tag,
+ value,
+ declares: `Font family ${JSON.stringify(fontFamily)} declares axis ${JSON.stringify(tag)} for ${definition.path}`,
+ }));
+ })
+ );
+}
+
+export function assertValidAxes(fontsByFamily: GroupedFontObject) {
+ for (const { tag, value, declares } of collectDeclaredAxes(fontsByFamily)) {
+ // On Android 15, `'wght' 900, 'sl t' -10` rendered at weight 900 with no slant: the bad
+ // entry is dropped, the rest still applies.
+ const consequence = `Android may drop a setting it cannot parse.`;
+ const lowercaseTag = tag.toLowerCase();
+
+ // An `app.config.ts` conditional writes `undefined` for the axis it leaves out.
+ if (value === undefined) {
+ continue;
+ }
+
+ if (tag.length !== AXIS_TAG_LENGTH) {
+ throw new Error(
+ `${declares}, which is not four characters. An axis tag is exactly four, such as "slnt". ` +
+ `List the axes a font declares with a utility such as fontTools: https://fonttools.readthedocs.io/`
+ );
+ }
+
+ if (!AXIS_TAG_PATTERN.test(tag)) {
+ throw new Error(
+ `${declares}, which holds characters no axis tag uses. A tag begins with a letter, then letters, digits, or the trailing spaces that pad a shorter tag. ${consequence}`
+ );
+ }
+
+ if (
+ registeredAxisTags.includes(lowercaseTag) &&
+ lowercaseTag !== tag &&
+ !isFoundryAxisTag(tag)
+ ) {
+ throw new Error(
+ `${declares}, which names no axis: tags are case sensitive. Write it as ${JSON.stringify(lowercaseTag)}. ${consequence}`
+ );
+ }
+
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
+ throw new Error(
+ `${declares}, which is set to ${JSON.stringify(value)} — not a finite number. ` +
+ `An axis takes a number in the range the font declares, such as -10 for "slnt".`
+ );
+ }
+ }
+}
+
+export function warnAboutUnknownAxisTags(fontsByFamily: GroupedFontObject) {
+ for (const { tag, value, declares } of collectDeclaredAxes(fontsByFamily)) {
+ if (value === undefined) {
+ continue;
+ }
+
+ const lowercaseTag = tag.toLowerCase();
+
+ // A font may declare `SLNT`, so this stays legal — but it is far more often `slnt` in the
+ // wrong case, and Android then applies nothing.
+ if (isFoundryAxisTag(tag) && registeredAxisTags.includes(lowercaseTag)) {
+ WarningAggregator.addWarningAndroid(
+ 'expo-font',
+ `${declares}, which is an axis the font declares for itself, not the registered ${JSON.stringify(lowercaseTag)}. ` +
+ `Write it as ${JSON.stringify(lowercaseTag)} instead if you meant the registered axis.`
+ );
+ } else if (!registeredAxisTags.includes(tag) && !isFoundryAxisTag(tag)) {
+ WarningAggregator.addWarningAndroid(
+ 'expo-font',
+ `${declares}, which names no axis: registered axes are lowercase, such as "slnt", and a font's own are uppercase, such as "GRAD". ` +
+ `Write it in the case the font uses, or Android applies nothing for it.`
+ );
+ }
+ }
+}
+
+// Either axis draws a slant: `slnt` by an angle, `ital` by the upright-to-italic switch.
+const slantAxisTags = ['ital', 'slnt'];
+
+function hasSlantAxis(definition: ResolvedFontDefinition) {
+ return Object.entries(definition.axes ?? {}).some(
+ ([tag, value]) => value !== undefined && slantAxisTags.includes(tag.toLowerCase())
+ );
+}
+
+/**
+ * Warns when one file backs both an upright and an italic face while the italic one sets no slant.
+ */
+export function warnAboutUnslantedItalics(fontsByFamily: GroupedFontObject) {
+ for (const [fontFamily, definitions] of Object.entries(fontsByFamily)) {
+ const uprightPaths = new Set(
+ definitions.filter((it) => (it.style || 'normal') === 'normal').map((it) => it.path)
+ );
+
+ for (const definition of definitions) {
+ // A font definition that has a file of its own may hold a static italic, which needs no axis.
+ const rendersUpright =
+ definition.style === 'italic' &&
+ uprightPaths.has(definition.path) &&
+ !hasSlantAxis(definition);
+
+ if (rendersUpright) {
+ WarningAggregator.addWarningAndroid(
+ 'expo-font',
+ `Font family ${JSON.stringify(fontFamily)} reads ${definition.path} for style "italic", and the same file also backs an upright face. ` +
+ `"style" by itself does not slant the glyphs, so the text will render upright. ` +
+ `Add a slant to "axes" for that definition, such as { "slnt": -10 }, or point it at an italic font file using "path".`
+ );
+ }
+ }
+ }
+}
+
+function formatVariationSettings(definition: FontDefinition) {
+ const { wght, ...rest } = definition.axes ?? {};
+ const axes = {
+ wght: wght ?? definition.weight,
+ ...rest,
+ };
+
+ return Object.entries(axes)
+ .filter(([, value]) => value !== undefined)
+ .map(([tag, value]) => `'${tag}' ${value}`)
+ .join(', ');
+}
+
function addXmlFonts(config: ExpoConfig, xmlFontObjects: FontObject[]) {
const fontsByFamily = groupByFamily(xmlFontObjects);
assertAndroidCanLoadFonts(fontsByFamily);
+ assertValidWeights(fontsByFamily);
assertNoConflictingDefinitions(fontsByFamily);
+ assertValidAxes(fontsByFamily);
+ warnAboutUnknownAxisTags(fontsByFamily);
+ warnAboutUnslantedItalics(fontsByFamily);
const fontPaths = Object.values(fontsByFamily).flatMap((definitions) =>
definitions.map((it) => it.path)
);
@@ -140,9 +355,9 @@ export function getXmlSpecs(fontsDir: string, xmlFontObjects: GroupedFontObject)
'app:font': `@font/${toValidAndroidResourceName(definition.path)}`,
'app:fontStyle': definition.style || 'normal',
'app:fontWeight': String(definition.weight),
- // Instances a variable font at the declared weight, so that one file can back
- // several definitions. Static fonts have no `wght` axis and ignore it.
- 'app:fontVariationSettings': `'wght' ${definition.weight}`,
+ // Instances a variable font at the declared weight and axes, so that one file can
+ // back several definitions. Static fonts declare no axes and ignore this.
+ 'app:fontVariationSettings': formatVariationSettings(definition),
},
};
}),
diff --git a/packages/expo-modules-autolinking/CHANGELOG.md b/packages/expo-modules-autolinking/CHANGELOG.md
index 502d92e0aa9096..8df80ac5bb958a 100644
--- a/packages/expo-modules-autolinking/CHANGELOG.md
+++ b/packages/expo-modules-autolinking/CHANGELOG.md
@@ -17,6 +17,7 @@
- [iOS] Fix a duplicate pod error (`multiple dependencies with different sources`) for precompiled modules in a Podfile with multiple targets. `prebuilt_react_active?` now mirrors React Native's default for `RCT_USE_PREBUILT_RNCORE` (prebuilt unless explicitly `0`), since `use_react_native!` only sets it after `use_expo_modules!` has run. ([#47329](https://github.com/expo/expo/pull/47329) by [@janicduplessis](https://github.com/janicduplessis))
- [Android] Resolve symlinks in the Gradle plugin `sourceDir` before it reaches `includeBuild`. Android Studio failed to sync with `Missing ExternalProject for :` in pnpm workspaces with a patched dependency. ([#48495](https://github.com/expo/expo/pull/48495) by [@lukmccall](https://github.com/lukmccall))
- Fixed unsorted autolinking result and introduced unstable fingerprint. ([#48629](https://github.com/expo/expo/pull/48629) by [@kudo](https://github.com/kudo))
+- [iOS] Stop discarding xcconfig changes made by other `post_install` hooks, which broke builds against React Native nightlies with `include of non-modular header inside framework module 'React.…'`. ([#49038](https://github.com/expo/expo/pull/49038) by [@kudo](https://github.com/kudo))
- [iOS] Fix `'React/RCTBridge.h' file not found` with `ios.useFrameworks: "dynamic"` by reverting the dynamic-framework linkage guard from [#47500](https://github.com/expo/expo/pull/47500). Under `:dynamic` linkage every pod target is already a dynamic framework, so the guard skipped the whole `USE_FRAMEWORKS` downgrade. `@rnmapbox/maps` 10.3.2 no longer needs the guard because it skips its own dynamic flip when precompiled modules are enabled. ([#48869](https://github.com/expo/expo/pull/48869) by [@kudo](https://github.com/kudo))
### 💡 Others
diff --git a/packages/expo-modules-autolinking/scripts/ios/project_integrator.rb b/packages/expo-modules-autolinking/scripts/ios/project_integrator.rb
index 11fe4161eeafdc..1f29ca7bf97acb 100644
--- a/packages/expo-modules-autolinking/scripts/ios/project_integrator.rb
+++ b/packages/expo-modules-autolinking/scripts/ios/project_integrator.rb
@@ -173,8 +173,12 @@ def self.integrate_core_macro_plugins(targets)
# Appends the macro plugin flags to the target's `OTHER_SWIFT_FLAGS` and saves the xcconfig,
# skipping it when the flags are already present.
+ #
+ # Read the file from disk rather than `build_settings.xcconfig`. Earlier `post_install`
+ # hooks write to it too — React Native's puts its module map flags there — and rebuilding
+ # it from CocoaPods' in-memory settings would throw those away.
def self.append_macro_flags(build_settings, xcconfig_path, macro_flags)
- xcconfig = build_settings.xcconfig
+ xcconfig = File.exist?(xcconfig_path) ? Xcodeproj::Config.new(xcconfig_path) : build_settings.xcconfig
swift_flags = xcconfig.attributes[SWIFT_FLAGS] || '$(inherited)'
return if swift_flags.include?(macro_flags)
diff --git a/packages/expo-notifications/CHANGELOG.md b/packages/expo-notifications/CHANGELOG.md
index 6a71f180556994..adc34f52ae5046 100644
--- a/packages/expo-notifications/CHANGELOG.md
+++ b/packages/expo-notifications/CHANGELOG.md
@@ -14,6 +14,7 @@
- [iOS] Avoid warning when an aborted push token registration request rejects with a native fetch cancellation error. ([#48547](https://github.com/expo/expo/pull/48547) by [@JoaoPauloCMarra](https://github.com/JoaoPauloCMarra))
- [Android] Prevent a crash on notification tap when `getLaunchIntentForPackage` throws on some OEM ROMs. ([#47889](https://github.com/expo/expo/pull/47889) by [@nunocaseiro](https://github.com/nunocaseiro))
- [web] Fixed crash when browser storage is blocked (e.g. Safari's "Block All Cookies"), where reading `localStorage` throws a `SecurityError` instead of returning `null`. ([#48033](https://github.com/expo/expo/pull/48033) by [@Ignigena](https://github.com/Ignigena))
+- [Android] Fixed importing `expo-notifications` crashing the app in Expo Go. ([#49062](https://github.com/expo/expo/pull/49062) by [@alanjhughes](https://github.com/alanjhughes))
### 💡 Others
diff --git a/packages/expo-notifications/src/DevicePushTokenAutoRegistration.fx.ts b/packages/expo-notifications/src/DevicePushTokenAutoRegistration.fx.ts
index edffca2f3a88a6..1ea7df2de8855a 100644
--- a/packages/expo-notifications/src/DevicePushTokenAutoRegistration.fx.ts
+++ b/packages/expo-notifications/src/DevicePushTokenAutoRegistration.fx.ts
@@ -1,5 +1,5 @@
import 'abort-controller/polyfill';
-import { UnavailabilityError } from 'expo';
+import { isRunningInExpoGo, Platform, UnavailabilityError } from 'expo';
import ServerRegistrationModule from './ServerRegistrationModule';
import { addPushTokenListener } from './TokenEmitter';
@@ -98,7 +98,12 @@ export async function __handlePersistedRegistrationInfoAsync(
}
}
-if (ServerRegistrationModule.getRegistrationInfoAsync) {
+if (isRunningInExpoGo() && Platform.OS === 'android') {
+ // Registering the module-scope push token listener would throw and make the import fatal.
+ console.warn(
+ '[expo-notifications] Push notifications (remote notifications) are unavailable in Expo Go on Android since SDK 53. Local notifications remain available. Use a development build for push notifications: https://docs.expo.dev/develop/development-builds/introduction/'
+ );
+} else if (ServerRegistrationModule.getRegistrationInfoAsync) {
// A global scope (to get all the updates) device push token
// subscription, never cleared.
addPushTokenListener(async (token) => {
diff --git a/packages/expo-notifications/src/__tests__/DevicePushTokenAutoRegistration-expoGo-test.ts b/packages/expo-notifications/src/__tests__/DevicePushTokenAutoRegistration-expoGo-test.ts
new file mode 100644
index 00000000000000..274bae852955e3
--- /dev/null
+++ b/packages/expo-notifications/src/__tests__/DevicePushTokenAutoRegistration-expoGo-test.ts
@@ -0,0 +1,28 @@
+// Expo Go on Android ships the native server registration module, but push
+// token usage throws there.
+jest.mock('expo', () => {
+ const expo = jest.requireActual('expo');
+ return {
+ ...expo,
+ isRunningInExpoGo: () => true,
+ Platform: { ...expo.Platform, OS: 'android' },
+ };
+});
+jest.mock('../ServerRegistrationModule', () => ({
+ __esModule: true,
+ default: {
+ addListener: () => {},
+ removeListeners: () => {},
+ getRegistrationInfoAsync: jest.fn().mockResolvedValue(null),
+ setRegistrationInfoAsync: jest.fn().mockResolvedValue(undefined),
+ },
+}));
+jest.mock('../utils/updateDevicePushTokenAsync');
+jest.mock('../getDevicePushTokenAsync');
+
+it('warns instead of throwing on import in Expo Go on Android', () => {
+ const spy = jest.spyOn(console, 'warn').mockImplementation();
+ expect(() => require('../DevicePushTokenAutoRegistration.fx')).not.toThrow();
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('unavailable in Expo Go on Android'));
+ spy.mockRestore();
+});
diff --git a/packages/expo-sqlite/.gitignore b/packages/expo-sqlite/.gitignore
index aca6cc2f451092..7849558a7db69f 100644
--- a/packages/expo-sqlite/.gitignore
+++ b/packages/expo-sqlite/.gitignore
@@ -2,5 +2,7 @@
/ios/sqlite3.c
/ios/sqlite3.h
-# DevTools plugin temporary output
+# DevTools plugin web UI output. Generated on demand during native builds and at
+# publish time, so it is not committed.
/dev-plugin-webui/dist
+/dev-plugin-dist
diff --git a/packages/expo-sqlite/.npmignore b/packages/expo-sqlite/.npmignore
index c162060d4bb2e5..00ad242409674a 100644
--- a/packages/expo-sqlite/.npmignore
+++ b/packages/expo-sqlite/.npmignore
@@ -17,11 +17,11 @@ __rsc_tests__
/ios/sqlite3.c
/ios/sqlite3.h
-# Scripts are only for sqlite3 source code prebuilding
+# Scripts only prebuild sqlite3 sources and the devtools plugin web UI
/scripts/
# Exclude source of dev-plugin-webui to reduce package size
/dev-plugin-webui/
-# Include all files in the bundled dev-plugin-dist
+# Include all files in dev-plugin-dist, which `prepublishOnly` generates
!/dev-plugin-dist/**/*
diff --git a/packages/expo-sqlite/.prettierignore b/packages/expo-sqlite/.prettierignore
index 6eb34c4ebe5e29..fc7da9c6afbbca 100644
--- a/packages/expo-sqlite/.prettierignore
+++ b/packages/expo-sqlite/.prettierignore
@@ -1,3 +1,2 @@
dev-plugin-dist/
-dev-plugin-webui/
web/wa-sqlite/
diff --git a/packages/expo-sqlite/CHANGELOG.md b/packages/expo-sqlite/CHANGELOG.md
index 98be6c7b4c3ea6..b68f14065dc6bf 100644
--- a/packages/expo-sqlite/CHANGELOG.md
+++ b/packages/expo-sqlite/CHANGELOG.md
@@ -4,15 +4,20 @@
### 🛠 Breaking changes
+- Removed libSQL support. `syncLibSQL()` and the `libSQLOptions` open option are gone, and the `useLibSQL` config plugin property is deprecated: it warns during prebuild and no longer has any effect. ([#48543](https://github.com/expo/expo/pull/48543) by [@kudo](https://github.com/kudo))
+
### 🎉 New features
### 🐛 Bug fixes
- [tvOS] Fix path for DB creation. ([#46715](https://github.com/expo/expo/pull/46715) by [@douglowder](https://github.com/douglowder))
+- Fixed the devtools plugin bundle missing its `wa-sqlite.wasm` asset. ([#48542](https://github.com/expo/expo/pull/48542) by [@kudo](https://github.com/kudo))
### 💡 Others
- Changed native implementation to use the unified `ArrayBuffer` type. ([#47168](https://github.com/expo/expo/pull/47168) by [@barthap](https://github.com/barthap))
+- Build the devtools plugin web UI from the monorepo workspace instead of a standalone project. ([#48542](https://github.com/expo/expo/pull/48542) by [@kudo](https://github.com/kudo))
+- Updated SQLite to 3.53.3 and SQLCipher to 4.17.0 on all platforms, including the web WebAssembly build. ([#48544](https://github.com/expo/expo/pull/48544) by [@kudo](https://github.com/kudo))
## 57.0.1 - 2026-07-15
diff --git a/packages/expo-sqlite/android/CMakeLists.txt b/packages/expo-sqlite/android/CMakeLists.txt
index c15cf925f58269..cecd9c5d708e57 100644
--- a/packages/expo-sqlite/android/CMakeLists.txt
+++ b/packages/expo-sqlite/android/CMakeLists.txt
@@ -14,40 +14,19 @@ add_compile_options(
${SQLITE_BUILDFLAGS}
)
-if(${USE_LIBSQL})
- file(GLOB SOURCES "${SRC_DIR}/libsql/*.cpp")
- add_library(
- ${PACKAGE_NAME}
- SHARED
- ${SOURCES}
- )
- target_include_directories(
- ${PACKAGE_NAME}
- PRIVATE
- "${CMAKE_SOURCE_DIR}/libsql"
- )
- find_library(
- LIBSQL_LIB
- sql_experimental
- PATHS "${CMAKE_SOURCE_DIR}/libsql/${ANDROID_ABI}"
- NO_CMAKE_FIND_ROOT_PATH
- )
-else()
- file(GLOB SOURCES "${SRC_DIR}/*.cpp")
- add_library(
- ${PACKAGE_NAME}
- SHARED
- ${SOURCES}
- "${SQLITE3_SRC_DIR}/sqlite3.c"
- )
- target_include_directories(
- ${PACKAGE_NAME}
- PRIVATE
- ${SRC_DIR}
- "${SQLITE3_SRC_DIR}"
- )
- set(LIBSQL_LIB "")
-endif()
+file(GLOB SOURCES "${SRC_DIR}/*.cpp")
+add_library(
+ ${PACKAGE_NAME}
+ SHARED
+ ${SOURCES}
+ "${SQLITE3_SRC_DIR}/sqlite3.c"
+)
+target_include_directories(
+ ${PACKAGE_NAME}
+ PRIVATE
+ ${SRC_DIR}
+ "${SQLITE3_SRC_DIR}"
+)
find_library(LOG_LIB log)
find_package(fbjni REQUIRED CONFIG)
@@ -62,7 +41,6 @@ target_link_libraries(
${PACKAGE_NAME}
${LOG_LIB}
${OPENSSL_CRYPTO_LIB}
- ${LIBSQL_LIB}
fbjni::fbjni
android
)
diff --git a/packages/expo-sqlite/android/build.gradle b/packages/expo-sqlite/android/build.gradle
index 55b1f96915e97e..01578dc3052a7d 100644
--- a/packages/expo-sqlite/android/build.gradle
+++ b/packages/expo-sqlite/android/build.gradle
@@ -15,7 +15,6 @@ String toPlatformIndependentPath(File path) {
ext {
USE_SQLCIPHER = findProperty('expo.sqlite.useSQLCipher') == 'true'
- USE_LIBSQL = findProperty('expo.sqlite.useLibSQL') == 'true'
WITH_SQLITE_VEC = findProperty('expo.sqlite.withSQLiteVecExtension') == 'true'
}
def SQLITE3_SRC_DIR = new File("${projectDir}/../vendor/sqlite3")
@@ -54,7 +53,6 @@ android {
defaultConfig {
versionCode 18
versionName "56.0.4"
- buildConfigField "boolean", "USE_LIBSQL", project.ext.USE_LIBSQL.toString()
buildConfigField "boolean", "WITH_SQLITE_VEC", project.ext.WITH_SQLITE_VEC.toString()
externalNativeBuild {
@@ -64,8 +62,7 @@ android {
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
"-DSQLITE3_SRC_DIR=${toPlatformIndependentPath(SQLITE3_SRC_DIR)}",
"-DSQLITE_BUILDFLAGS=${getSQLiteBuildFlags()}",
- "-DUSE_SQLCIPHER=${project.ext.USE_SQLCIPHER}",
- "-DUSE_LIBSQL=${project.ext.USE_LIBSQL}"
+ "-DUSE_SQLCIPHER=${project.ext.USE_SQLCIPHER}"
}
}
}
diff --git a/packages/expo-sqlite/android/libsql/arm64-v8a/libsql_experimental.so b/packages/expo-sqlite/android/libsql/arm64-v8a/libsql_experimental.so
deleted file mode 100755
index 69d7e330d230bd..00000000000000
Binary files a/packages/expo-sqlite/android/libsql/arm64-v8a/libsql_experimental.so and /dev/null differ
diff --git a/packages/expo-sqlite/android/libsql/armeabi-v7a/libsql_experimental.so b/packages/expo-sqlite/android/libsql/armeabi-v7a/libsql_experimental.so
deleted file mode 100755
index 7fdd6b480113f4..00000000000000
Binary files a/packages/expo-sqlite/android/libsql/armeabi-v7a/libsql_experimental.so and /dev/null differ
diff --git a/packages/expo-sqlite/android/libsql/libsql.h b/packages/expo-sqlite/android/libsql/libsql.h
deleted file mode 100644
index 505c4b291d9166..00000000000000
--- a/packages/expo-sqlite/android/libsql/libsql.h
+++ /dev/null
@@ -1,173 +0,0 @@
-#ifndef LIBSQL_EXPERIMENTAL_H
-#define LIBSQL_EXPERIMENTAL_H
-
-#include
-
-#define LIBSQL_INT 1
-
-#define LIBSQL_FLOAT 2
-
-#define LIBSQL_TEXT 3
-
-#define LIBSQL_BLOB 4
-
-#define LIBSQL_NULL 5
-
-typedef struct libsql_connection libsql_connection;
-
-typedef struct libsql_database libsql_database;
-
-typedef struct libsql_row libsql_row;
-
-typedef struct libsql_rows libsql_rows;
-
-typedef struct libsql_rows_future libsql_rows_future;
-
-typedef struct libsql_stmt libsql_stmt;
-
-typedef const libsql_database *libsql_database_t;
-
-typedef struct {
- int frame_no;
- int frames_synced;
-} replicated;
-
-typedef struct {
- const char *db_path;
- const char *primary_url;
- const char *auth_token;
- char read_your_writes;
- const char *encryption_key;
- int sync_interval;
- char with_webpki;
- char offline;
-} libsql_config;
-
-typedef const libsql_connection *libsql_connection_t;
-
-typedef const libsql_stmt *libsql_stmt_t;
-
-typedef const libsql_rows *libsql_rows_t;
-
-typedef const libsql_rows_future *libsql_rows_future_t;
-
-typedef const libsql_row *libsql_row_t;
-
-typedef struct {
- const char *ptr;
- int len;
-} blob;
-
-#ifdef __cplusplus
-extern "C" {
-#endif // __cplusplus
-
-int libsql_enable_internal_tracing(void);
-
-int libsql_sync(libsql_database_t db, const char **out_err_msg);
-
-int libsql_sync2(libsql_database_t db, replicated *out_replicated, const char **out_err_msg);
-
-int libsql_open_sync(const char *db_path,
- const char *primary_url,
- const char *auth_token,
- char read_your_writes,
- const char *encryption_key,
- libsql_database_t *out_db,
- const char **out_err_msg);
-
-int libsql_open_sync_with_webpki(const char *db_path,
- const char *primary_url,
- const char *auth_token,
- char read_your_writes,
- const char *encryption_key,
- libsql_database_t *out_db,
- const char **out_err_msg);
-
-int libsql_open_sync_with_config(libsql_config config, libsql_database_t *out_db, const char **out_err_msg);
-
-int libsql_open_ext(const char *url, libsql_database_t *out_db, const char **out_err_msg);
-
-int libsql_open_file(const char *url, libsql_database_t *out_db, const char **out_err_msg);
-
-int libsql_open_remote(const char *url, const char *auth_token, libsql_database_t *out_db, const char **out_err_msg);
-
-int libsql_open_remote_with_webpki(const char *url,
- const char *auth_token,
- libsql_database_t *out_db,
- const char **out_err_msg);
-
-void libsql_close(libsql_database_t db);
-
-int libsql_connect(libsql_database_t db, libsql_connection_t *out_conn, const char **out_err_msg);
-
-int libsql_load_extension(libsql_connection_t conn,
- const char *path,
- const char *entry_point,
- const char **out_err_msg);
-
-int libsql_reset(libsql_connection_t conn, const char **out_err_msg);
-
-void libsql_disconnect(libsql_connection_t conn);
-
-int libsql_prepare(libsql_connection_t conn, const char *sql, libsql_stmt_t *out_stmt, const char **out_err_msg);
-
-int libsql_bind_int(libsql_stmt_t stmt, int idx, long long value, const char **out_err_msg);
-
-int libsql_bind_float(libsql_stmt_t stmt, int idx, double value, const char **out_err_msg);
-
-int libsql_bind_null(libsql_stmt_t stmt, int idx, const char **out_err_msg);
-
-int libsql_bind_string(libsql_stmt_t stmt, int idx, const char *value, const char **out_err_msg);
-
-int libsql_bind_blob(libsql_stmt_t stmt, int idx, const unsigned char *value, int value_len, const char **out_err_msg);
-
-int libsql_query_stmt(libsql_stmt_t stmt, libsql_rows_t *out_rows, const char **out_err_msg);
-
-int libsql_execute_stmt(libsql_stmt_t stmt, const char **out_err_msg);
-
-int libsql_reset_stmt(libsql_stmt_t stmt, const char **out_err_msg);
-
-void libsql_free_stmt(libsql_stmt_t stmt);
-
-int libsql_query(libsql_connection_t conn, const char *sql, libsql_rows_t *out_rows, const char **out_err_msg);
-
-int libsql_execute(libsql_connection_t conn, const char *sql, const char **out_err_msg);
-
-void libsql_free_rows(libsql_rows_t res);
-
-void libsql_free_rows_future(libsql_rows_future_t res);
-
-void libsql_wait_result(libsql_rows_future_t res);
-
-int libsql_column_count(libsql_rows_t res);
-
-int libsql_column_name(libsql_rows_t res, int col, const char **out_name, const char **out_err_msg);
-
-int libsql_column_type(libsql_rows_t res, libsql_row_t row, int col, int *out_type, const char **out_err_msg);
-
-uint64_t libsql_changes(libsql_connection_t conn);
-
-int64_t libsql_last_insert_rowid(libsql_connection_t conn);
-
-int libsql_next_row(libsql_rows_t res, libsql_row_t *out_row, const char **out_err_msg);
-
-void libsql_free_row(libsql_row_t res);
-
-int libsql_get_string(libsql_row_t res, int col, const char **out_value, const char **out_err_msg);
-
-void libsql_free_string(const char *ptr);
-
-int libsql_get_int(libsql_row_t res, int col, long long *out_value, const char **out_err_msg);
-
-int libsql_get_float(libsql_row_t res, int col, double *out_value, const char **out_err_msg);
-
-int libsql_get_blob(libsql_row_t res, int col, blob *out_blob, const char **out_err_msg);
-
-void libsql_free_blob(blob b);
-
-#ifdef __cplusplus
-} // extern "C"
-#endif // __cplusplus
-
-#endif /* LIBSQL_EXPERIMENTAL_H */
diff --git a/packages/expo-sqlite/android/libsql/x86/libsql_experimental.so b/packages/expo-sqlite/android/libsql/x86/libsql_experimental.so
deleted file mode 100755
index cf1b425deefd37..00000000000000
Binary files a/packages/expo-sqlite/android/libsql/x86/libsql_experimental.so and /dev/null differ
diff --git a/packages/expo-sqlite/android/libsql/x86_64/libsql_experimental.so b/packages/expo-sqlite/android/libsql/x86_64/libsql_experimental.so
deleted file mode 100755
index ebbf70cfd5bc10..00000000000000
Binary files a/packages/expo-sqlite/android/libsql/x86_64/libsql_experimental.so and /dev/null differ
diff --git a/packages/expo-sqlite/android/shouldUsePublication.groovy b/packages/expo-sqlite/android/shouldUsePublication.groovy
index 4fc72bbbb3dbdc..ff21148397f414 100644
--- a/packages/expo-sqlite/android/shouldUsePublication.groovy
+++ b/packages/expo-sqlite/android/shouldUsePublication.groovy
@@ -1,6 +1,5 @@
[
"expo.sqlite.useSQLCipher",
- "expo.sqlite.useLibSQL",
"expo.sqlite.enableFTS",
"expo.sqlite.customBuildFlags",
"expo.sqlite.withSQLiteVecExtension",
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/Exceptions.h b/packages/expo-sqlite/android/src/main/cpp/libsql/Exceptions.h
deleted file mode 100644
index 926799954c95f2..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/Exceptions.h
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#pragma once
-
-#include
-#include
-
-namespace jni = facebook::jni;
-
-namespace expo {
-
-/**
- * A convenient wrapper for the Kotlin CodedException.
- * TODO: Add prefabPublishing from expo-modules-core and remove the duplicated
- * definition.
- */
-class CodedException : public jni::JavaClass {
-public:
- static auto constexpr kJavaDescriptor =
- "Lexpo/modules/kotlin/exception/CodedException;";
-
- static jni::local_ref create(const std::string &message);
-};
-
-/**
- * A convenient wrapper for the Kotlin SQLiteErrorException.
- */
-class SQLiteErrorException
- : public jni::JavaClass {
-public:
- static auto constexpr kJavaDescriptor =
- "Lexpo/modules/sqlite/SQLiteErrorException;";
-
- static jni::local_ref create(const char *message) {
- if (message) {
- return SQLiteErrorException::newInstance(jni::make_jstring(message));
- }
- return SQLiteErrorException::newInstance(
- jni::make_jstring("Unknown error"));
- }
-
- static jni::local_ref
- create(const std::string &message) {
- return SQLiteErrorException::newInstance(jni::make_jstring(message));
- }
-
- static jni::local_ref
- create(jni::alias_ref message) {
- return SQLiteErrorException::newInstance(message);
- }
-};
-
-/**
- * A convenient wrapper for the Kotlin UnsupportedOperationException.
- */
-class UnsupportedOperationException
- : public jni::JavaClass {
-public:
- static auto constexpr kJavaDescriptor =
- "Lexpo/modules/sqlite/UnsupportedOperationException;";
-
- static jni::local_ref create() {
- return UnsupportedOperationException::newInstance(jni::make_jstring(""));
- }
-
- static jni::local_ref
- create(const std::string &message) {
- return UnsupportedOperationException::newInstance(
- jni::make_jstring(message));
- }
-
- static jni::local_ref
- create(const jni::alias_ref &message) {
- return UnsupportedOperationException::newInstance(message);
- }
-};
-
-/**
- * A convenient wrapper for the Kotlin InvalidConvertibleException.
- */
-class InvalidConvertibleException
- : public jni::JavaClass {
-public:
- static auto constexpr kJavaDescriptor =
- "Lexpo/modules/sqlite/InvalidConvertibleException;";
-
- static jni::local_ref
- create(const std::string &message) {
- return InvalidConvertibleException::newInstance(jni::make_jstring(message));
- }
-};
-
-} // namespace expo
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeDatabaseBinding.cpp b/packages/expo-sqlite/android/src/main/cpp/libsql/NativeDatabaseBinding.cpp
deleted file mode 100644
index aec718f30b4e0f..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeDatabaseBinding.cpp
+++ /dev/null
@@ -1,227 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#include "NativeDatabaseBinding.h"
-
-#include "Exceptions.h"
-
-namespace jni = facebook::jni;
-
-namespace expo {
-
-namespace {
-
-constexpr char TAG[] = "expo-sqlite";
-
-} // namespace
-
-// static
-void NativeDatabaseBinding::registerNatives() {
- registerHybrid({
- makeNativeMethod("initHybrid", NativeDatabaseBinding::initHybrid),
- makeNativeMethod("sqlite3_changes",
- NativeDatabaseBinding::sqlite3_changes),
- makeNativeMethod("sqlite3_close", NativeDatabaseBinding::sqlite3_close),
- makeNativeMethod("sqlite3_db_filename",
- NativeDatabaseBinding::sqlite3_db_filename),
- makeNativeMethod("sqlite3_enable_load_extension",
- NativeDatabaseBinding::sqlite3_enable_load_extension),
- makeNativeMethod("sqlite3_exec", NativeDatabaseBinding::sqlite3_exec),
- makeNativeMethod("sqlite3_get_autocommit",
- NativeDatabaseBinding::sqlite3_get_autocommit),
- makeNativeMethod("sqlite3_last_insert_rowid",
- NativeDatabaseBinding::sqlite3_last_insert_rowid),
- makeNativeMethod("sqlite3_load_extension",
- NativeDatabaseBinding::sqlite3_load_extension),
- makeNativeMethod("sqlite3_open", NativeDatabaseBinding::sqlite3_open),
- makeNativeMethod("sqlite3_prepare_v2",
- NativeDatabaseBinding::sqlite3_prepare_v2),
- makeNativeMethod("sqlite3_serialize",
- NativeDatabaseBinding::sqlite3_serialize),
- makeNativeMethod("sqlite3_deserialize",
- NativeDatabaseBinding::sqlite3_deserialize),
- makeNativeMethod("sqlite3_update_hook",
- NativeDatabaseBinding::sqlite3_update_hook),
- makeNativeMethod("sqlite3_backup", NativeDatabaseBinding::sqlite3_backup),
- makeNativeMethod("libsql_open_remote",
- NativeDatabaseBinding::libsql_open_remote),
- makeNativeMethod("libsql_open", NativeDatabaseBinding::libsql_open),
- makeNativeMethod("libsql_sync", NativeDatabaseBinding::libsql_sync),
- makeNativeMethod("convertSqlLiteErrorToString",
- NativeDatabaseBinding::convertSqlLiteErrorToString),
- });
-}
-
-int NativeDatabaseBinding::sqlite3_changes() {
- return static_cast(::libsql_changes(conn));
-}
-
-int NativeDatabaseBinding::sqlite3_close() {
- ::libsql_disconnect(conn);
- ::libsql_close(db);
- return 0;
-}
-
-std::string
-NativeDatabaseBinding::sqlite3_db_filename(const std::string &databaseName) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return "";
-}
-
-int NativeDatabaseBinding::sqlite3_enable_load_extension(int onoff) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int NativeDatabaseBinding::sqlite3_exec(const std::string &source) {
- const char *errMsg;
- int ret = libsql_execute(conn, source.c_str(), &errMsg);
- if (ret != 0 && errMsg) {
- std::string errorString(errMsg);
- jni::throwNewJavaException(SQLiteErrorException::create(errorString).get());
- }
- return ret;
-}
-
-int NativeDatabaseBinding::sqlite3_get_autocommit() {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int64_t NativeDatabaseBinding::sqlite3_last_insert_rowid() {
- return ::libsql_last_insert_rowid(conn);
-}
-
-int NativeDatabaseBinding::sqlite3_load_extension(
- const std::string &libPath, const std::string &entryProc) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int NativeDatabaseBinding::sqlite3_open(const std::string &dbPath) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int NativeDatabaseBinding::sqlite3_prepare_v2(
- const std::string &source,
- jni::alias_ref statement) {
- NativeStatementBinding *cStatement = cthis(statement);
- const char *errMsg;
- if (::libsql_prepare(conn, source.c_str(), &cStatement->stmt, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return -1;
- }
- return 0;
-}
-
-jni::local_ref
-NativeDatabaseBinding::sqlite3_serialize(const std::string &databaseName) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return nullptr;
-}
-
-int NativeDatabaseBinding::sqlite3_deserialize(
- const std::string &databaseName,
- jni::alias_ref serializedData) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-void NativeDatabaseBinding::sqlite3_update_hook(bool enabled) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
-}
-
-// static
-int NativeDatabaseBinding::sqlite3_backup(
- jni::alias_ref clazz,
- jni::alias_ref destDatabase,
- const std::string &destDatabaseName,
- jni::alias_ref sourceDatabase,
- const std::string &sourceDatabaseName) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int NativeDatabaseBinding::libsql_open_remote(const std::string &url,
- const std::string &authToken) {
- const char *errMsg;
- int ret = ::libsql_open_remote(url.c_str(), authToken.c_str(), &db, &errMsg);
- if (ret != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return -1;
- }
- ret = ::libsql_connect(db, &conn, &errMsg);
- if (ret != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return -1;
- }
- return 0;
-}
-
-int NativeDatabaseBinding::libsql_open(const std::string &dbPath,
- const std::string &url,
- const std::string &authToken) {
- const char *errMsg;
- libsql_config config = {
- .db_path = dbPath.c_str(),
- .primary_url = url.c_str(),
- .auth_token = authToken.c_str(),
- .read_your_writes = 1,
- .encryption_key = nullptr,
- .sync_interval = 0,
- .with_webpki = 1,
- .offline = 1,
- };
- int ret = ::libsql_open_sync_with_config(config, &db, &errMsg);
- if (ret != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return -1;
- }
- ret = ::libsql_connect(db, &conn, &errMsg);
- if (ret != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return -1;
- }
- return 0;
-}
-
-int NativeDatabaseBinding::libsql_sync() {
- const char *errMsg;
- int ret = ::libsql_sync(db, &errMsg);
- if (ret != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return ret;
- }
- return 0;
-}
-
-std::string NativeDatabaseBinding::convertSqlLiteErrorToSTLString() {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return nullptr;
-}
-
-jni::local_ref
-NativeDatabaseBinding::convertSqlLiteErrorToString() {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return nullptr;
-}
-
-// static
-jni::local_ref
-NativeDatabaseBinding::initHybrid(jni::alias_ref jThis) {
- return makeCxxInstance(jThis);
-}
-
-// static
-void NativeDatabaseBinding::OnUpdateHook(void *arg, int action,
- char const *databaseName,
- char const *tableName, int64_t rowId) {
- NativeDatabaseBinding *pThis = reinterpret_cast(arg);
- static const auto method =
- jni::findClassStatic("expo/modules/sqlite/NativeDatabaseBinding")
- ->getMethod("onUpdate");
- method(pThis->javaPart_, action, jni::make_jstring(databaseName).get(),
- jni::make_jstring(tableName).get(), rowId);
-}
-
-} // namespace expo
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeDatabaseBinding.h b/packages/expo-sqlite/android/src/main/cpp/libsql/NativeDatabaseBinding.h
deleted file mode 100644
index 07f7a4f56745ae..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeDatabaseBinding.h
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#pragma once
-
-#include
-#include
-
-#include "NativeStatementBinding.h"
-#include "libsql.h"
-
-namespace jni = facebook::jni;
-
-namespace expo {
-
-class NativeDatabaseBinding : public jni::HybridClass {
-public:
- static constexpr auto kJavaDescriptor =
- "Lexpo/modules/sqlite/NativeDatabaseBinding;";
-
- static void registerNatives();
-
- // sqlite3 bindings
- int sqlite3_changes();
- int sqlite3_close();
- std::string sqlite3_db_filename(const std::string &databaseName);
- int sqlite3_enable_load_extension(int onoff);
- int sqlite3_exec(const std::string &source);
- int sqlite3_get_autocommit();
- int64_t sqlite3_last_insert_rowid();
- int sqlite3_load_extension(const std::string &libPath,
- const std::string &entryProc);
- int sqlite3_open(const std::string &dbPath);
- int sqlite3_prepare_v2(
- const std::string &source,
- jni::alias_ref statement);
- jni::local_ref
- sqlite3_serialize(const std::string &databaseName);
- int sqlite3_deserialize(const std::string &databaseName,
- jni::alias_ref serializedData);
- void sqlite3_update_hook(bool enabled);
-
- static int sqlite3_backup(
- jni::alias_ref clazz,
- jni::alias_ref destDatabase,
- const std::string &destDatabaseName,
- jni::alias_ref sourceDatabase,
- const std::string &sourceDatabaseName);
-
- int libsql_open_remote(const std::string &url, const std::string &authToken);
- int libsql_open(const std::string &dbPath, const std::string &url,
- const std::string &authToken);
- int libsql_sync();
-
- // helpers
- jni::local_ref convertSqlLiteErrorToString();
-
-private:
- explicit NativeDatabaseBinding(
- jni::alias_ref jThis)
- : javaPart_(jni::make_global(jThis)) {}
-
- std::string convertSqlLiteErrorToSTLString();
-
-private:
- static jni::local_ref
- initHybrid(jni::alias_ref jThis);
-
- static void OnUpdateHook(void *arg, int action, char const *databaseName,
- char const *tableName, int64_t rowId);
-
-private:
- friend HybridBase;
-
- jni::global_ref javaPart_;
- libsql_database_t db = nullptr;
- libsql_connection_t conn = nullptr;
-};
-
-} // namespace expo
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeSessionBinding.cpp b/packages/expo-sqlite/android/src/main/cpp/libsql/NativeSessionBinding.cpp
deleted file mode 100644
index 6ff0272bf0b7e6..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeSessionBinding.cpp
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#include "NativeSessionBinding.h"
-
-#include "Exceptions.h"
-
-namespace expo {
-
-void NativeSessionBinding::registerNatives() {
- registerHybrid({
- makeNativeMethod("initHybrid", NativeSessionBinding::initHybrid),
- makeNativeMethod("sqlite3session_create",
- NativeSessionBinding::sqlite3session_create),
- makeNativeMethod("sqlite3session_attach",
- NativeSessionBinding::sqlite3session_attach),
- makeNativeMethod("sqlite3session_enable",
- NativeSessionBinding::sqlite3session_enable),
- makeNativeMethod("sqlite3session_delete",
- NativeSessionBinding::sqlite3session_delete),
- makeNativeMethod("sqlite3session_changeset",
- NativeSessionBinding::sqlite3session_changeset),
- makeNativeMethod("sqlite3session_changeset_inverted",
- NativeSessionBinding::sqlite3session_changeset_inverted),
- makeNativeMethod("sqlite3changeset_apply",
- NativeSessionBinding::sqlite3changeset_apply),
- makeNativeMethod("sqlite3changeset_invert",
- NativeSessionBinding::sqlite3changeset_invert),
- });
-}
-
-// static
-jni::local_ref
-NativeSessionBinding::initHybrid(jni::alias_ref jThis) {
- return makeCxxInstance(jThis);
-}
-
-int NativeSessionBinding::sqlite3session_create(
- jni::alias_ref db,
- const std::string &dbName) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int NativeSessionBinding::sqlite3session_attach(
- jni::alias_ref tableName) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-int NativeSessionBinding::sqlite3session_enable(bool enabled) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-void NativeSessionBinding::sqlite3session_delete() {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
-}
-
-jni::local_ref
-NativeSessionBinding::sqlite3session_changeset() {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return nullptr;
-}
-
-jni::local_ref
-NativeSessionBinding::sqlite3session_changeset_inverted() {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return nullptr;
-}
-
-int NativeSessionBinding::sqlite3changeset_apply(
- jni::alias_ref db,
- jni::alias_ref changeset) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return -1;
-}
-
-jni::local_ref NativeSessionBinding::sqlite3changeset_invert(
- jni::alias_ref changeset) {
- jni::throwNewJavaException(UnsupportedOperationException::create().get());
- return nullptr;
-}
-
-} // namespace expo
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeSessionBinding.h b/packages/expo-sqlite/android/src/main/cpp/libsql/NativeSessionBinding.h
deleted file mode 100644
index 19ecbf4c2bae74..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeSessionBinding.h
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#pragma once
-
-#include
-#include
-#include
-
-#include "NativeDatabaseBinding.h"
-
-namespace jni = facebook::jni;
-
-namespace expo {
-
-class NativeSessionBinding : public jni::HybridClass {
-public:
- static constexpr auto kJavaDescriptor =
- "Lexpo/modules/sqlite/NativeSessionBinding;";
-
- static void registerNatives();
-
- // sqlite3session bindings
- int sqlite3session_create(
- jni::alias_ref db,
- const std::string &dbName);
- int sqlite3session_attach(jni::alias_ref tableName);
- int sqlite3session_enable(bool enabled);
- void sqlite3session_delete();
- jni::local_ref sqlite3session_changeset();
- jni::local_ref sqlite3session_changeset_inverted();
- int sqlite3changeset_apply(
- jni::alias_ref db,
- jni::alias_ref changeset);
- jni::local_ref
- sqlite3changeset_invert(jni::alias_ref changeset);
-
-private:
- explicit NativeSessionBinding(
- jni::alias_ref jThis) {}
-
-private:
- static jni::local_ref
- initHybrid(jni::alias_ref jThis);
-
-private:
- friend HybridBase;
- friend NativeDatabaseBinding;
-};
-
-} // namespace expo
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeStatementBinding.cpp b/packages/expo-sqlite/android/src/main/cpp/libsql/NativeStatementBinding.cpp
deleted file mode 100644
index 0eebc58327fa27..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeStatementBinding.cpp
+++ /dev/null
@@ -1,255 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#include "NativeStatementBinding.h"
-
-#include "Exceptions.h"
-#include
-
-namespace jni = facebook::jni;
-
-// These return values should be synchronous with NativeDatabaseBinding.kt,
-// as we share Kotlin code with SQLite.
-#define SQLITE_ROW 100
-#define SQLITE_DONE 101
-
-namespace expo {
-
-namespace {
-
-constexpr char TAG[] = "expo-sqlite";
-
-} // namespace
-
-// static
-void NativeStatementBinding::registerNatives() {
- registerHybrid({
- makeNativeMethod("initHybrid", NativeStatementBinding::initHybrid),
- makeNativeMethod("sqlite3_bind_parameter_index",
- NativeStatementBinding::sqlite3_bind_parameter_index),
- makeNativeMethod("sqlite3_clear_bindings",
- NativeStatementBinding::sqlite3_clear_bindings),
- makeNativeMethod("sqlite3_column_count",
- NativeStatementBinding::sqlite3_column_count),
- makeNativeMethod("sqlite3_column_name",
- NativeStatementBinding::sqlite3_column_name),
- makeNativeMethod("sqlite3_finalize",
- NativeStatementBinding::sqlite3_finalize),
- makeNativeMethod("sqlite3_reset", NativeStatementBinding::sqlite3_reset),
- makeNativeMethod("sqlite3_step", NativeStatementBinding::sqlite3_step),
- makeNativeMethod("bindStatementParam",
- NativeStatementBinding::bindStatementParam),
- makeNativeMethod("getColumnNames",
- NativeStatementBinding::getColumnNames),
- makeNativeMethod("getColumnValues",
- NativeStatementBinding::getColumnValues),
- });
-}
-
-int NativeStatementBinding::sqlite3_bind_parameter_index(
- const std::string &name) {
- jni::throwNewJavaException(
- UnsupportedOperationException::create(
- "Named parameter binding is not supported in libSQL mode.")
- .get());
- return -1;
-}
-
-int NativeStatementBinding::sqlite3_clear_bindings() { return -1; }
-
-int NativeStatementBinding::sqlite3_column_count() {
- return ::libsql_column_count(getRows());
-}
-
-std::string NativeStatementBinding::sqlite3_column_name(int index) {
- const char *name;
- const char *errMsg;
- ::libsql_column_name(getRows(), index, &name, &errMsg);
- if (name) {
- return name;
- }
- return "Unknown column name";
-}
-
-int NativeStatementBinding::sqlite3_finalize() {
- if (currentRow) {
- ::libsql_free_row(currentRow);
- currentRow = nullptr;
- }
- if (rows) {
- ::libsql_free_rows(rows);
- rows = nullptr;
- }
- ::libsql_free_stmt(stmt);
- return 0;
-}
-
-int NativeStatementBinding::sqlite3_reset() {
- const char *errMsg;
- if (rows) {
- ::libsql_free_rows(rows);
- rows = nullptr;
- }
- if (::libsql_reset_stmt(stmt, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- return 0;
-}
-
-int NativeStatementBinding::sqlite3_step() {
- const char *errMsg;
- if (currentRow) {
- ::libsql_free_row(currentRow);
- currentRow = nullptr;
- }
- if (libsql_next_row(getRows(), ¤tRow, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- return currentRow != nullptr ? SQLITE_ROW : SQLITE_DONE;
-}
-
-int NativeStatementBinding::bindStatementParam(
- int index, jni::alias_ref param) {
- const char *errMsg;
-
- if (param == nullptr) {
- return ::libsql_bind_null(stmt, index, &errMsg);
- }
-
- if (param->isInstanceOf(jni::JInteger::javaClassStatic())) {
- return ::libsql_bind_int(
- stmt, index, jni::static_ref_cast(param)->value(),
- &errMsg);
- }
-
- if (param->isInstanceOf(jni::JLong::javaClassStatic())) {
- return ::libsql_bind_int(
- stmt, index, jni::static_ref_cast(param)->value(), &errMsg);
- }
-
- if (param->isInstanceOf(jni::JDouble::javaClassStatic())) {
- return ::libsql_bind_float(
- stmt, index, jni::static_ref_cast(param)->value(),
- &errMsg);
- }
-
- if (param->isInstanceOf(jni::JBoolean::javaClassStatic())) {
- return ::libsql_bind_int(
- stmt, index,
- jni::static_ref_cast(param)->value() ? 1 : 0, &errMsg);
- }
-
- if (param->isInstanceOf(jni::JArrayByte::javaClassStatic())) {
- auto byteArray = jni::static_ref_cast(param);
- auto data = byteArray->getRegion(0, byteArray->size());
- return ::libsql_bind_blob(
- stmt, index, reinterpret_cast(data.get()),
- byteArray->size(), &errMsg);
- }
-
- std::string stringArg;
- if (param->isInstanceOf(jni::JString::javaClassStatic())) {
- stringArg = jni::static_ref_cast(param)->toStdString();
- } else {
- stringArg = param->toString();
- }
- return ::libsql_bind_string(stmt, index, stringArg.c_str(), &errMsg);
-}
-
-jni::local_ref>
-NativeStatementBinding::getColumnNames() {
- int columnCount = this->sqlite3_column_count();
- auto columnNames = jni::JArrayList::create(columnCount);
- for (int i = 0; i < columnCount; ++i) {
- columnNames->add(jni::make_jstring(this->sqlite3_column_name(i)));
- }
- return columnNames;
-}
-
-jni::local_ref>
-NativeStatementBinding::getColumnValues() {
- int columnCount = this->sqlite3_column_count();
- auto columnValues = jni::JArrayList::create(columnCount);
- for (int i = 0; i < columnCount; ++i) {
- columnValues->add(getColumnValue(i));
- }
- return columnValues;
-}
-
-// static
-jni::local_ref
-NativeStatementBinding::initHybrid(jni::alias_ref jThis) {
- return makeCxxInstance(jThis);
-}
-
-jni::local_ref NativeStatementBinding::getColumnValue(int index) {
- const char *errMsg;
- int type;
- if (::libsql_column_type(getRows(), currentRow, index, &type, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- return nullptr;
- }
-
- switch (type) {
- case LIBSQL_INT: {
- long long value;
- if (::libsql_get_int(currentRow, index, &value, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- return jni::JLong::valueOf(value);
- }
- case LIBSQL_FLOAT: {
- double value;
- if (::libsql_get_float(currentRow, index, &value, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- return jni::JDouble::valueOf(value);
- }
- case LIBSQL_TEXT: {
- const char *value;
- if (::libsql_get_string(currentRow, index, &value, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- auto result = jni::make_jstring(value);
- ::libsql_free_string(value);
- return result;
- }
- case LIBSQL_BLOB: {
- blob value;
- if (::libsql_get_blob(currentRow, index, &value, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- auto byteArray = jni::JArrayByte::newArray(value.len);
- byteArray->setRegion(0, value.len,
- reinterpret_cast(value.ptr));
- ::libsql_free_blob(value);
- return byteArray;
- }
- case LIBSQL_NULL: {
- return nullptr;
- }
- default: {
- std::string errorMessage =
- "Unsupported parameter type: " + std::to_string(type);
- jni::throwNewJavaException(
- InvalidConvertibleException::create(errorMessage).get());
- return nullptr;
- }
- }
- return nullptr;
-}
-
-libsql_rows_t NativeStatementBinding::getRows() {
- if (!rows) {
- const char *errMsg;
- if (::libsql_query_stmt(stmt, &rows, &errMsg) != 0) {
- jni::throwNewJavaException(SQLiteErrorException::create(errMsg).get());
- }
- if (!rows) {
- jni::throwNewJavaException(
- SQLiteErrorException::create("libsql_query_stmt returns null rows")
- .get());
- }
- }
- return rows;
-}
-} // namespace expo
diff --git a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeStatementBinding.h b/packages/expo-sqlite/android/src/main/cpp/libsql/NativeStatementBinding.h
deleted file mode 100644
index a32cbcb342f362..00000000000000
--- a/packages/expo-sqlite/android/src/main/cpp/libsql/NativeStatementBinding.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2015-present 650 Industries. All rights reserved.
-
-#pragma once
-
-#include
-#include
-
-#include "libsql.h"
-
-namespace jni = facebook::jni;
-
-namespace expo {
-
-class NativeDatabaseBinding;
-
-class NativeStatementBinding : public jni::HybridClass {
-public:
- static constexpr auto kJavaDescriptor =
- "Lexpo/modules/sqlite/NativeStatementBinding;";
-
- static void registerNatives();
-
- // sqlite3 bindings
- int sqlite3_bind_parameter_index(const std::string &name);
- int sqlite3_clear_bindings();
- int sqlite3_column_count();
- std::string sqlite3_column_name(int index);
- int sqlite3_finalize();
- int sqlite3_reset();
- int sqlite3_step();
-
- // helpers
- int bindStatementParam(int index, jni::alias_ref param);
- jni::local_ref> getColumnNames();
- jni::local_ref> getColumnValues();
-
-private:
- explicit NativeStatementBinding(
- jni::alias_ref jThis) {}
-
- jni::local_ref