feat: resolve mobile git native module
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"expo-auth-session": "~55.0.15",
|
||||
"expo-dev-client": "~55.0.32",
|
||||
"expo-file-system": "55.0.17",
|
||||
"expo-modules-core": "55.0.24",
|
||||
"expo-secure-store": "~55.0.13",
|
||||
"expo-status-bar": "~55.0.5",
|
||||
"expo-web-browser": "~55.0.14",
|
||||
|
||||
@@ -66,6 +66,7 @@ import { useMobileVaultRuntimeLoader } from './useMobileVaultRuntimeLoader'
|
||||
import type { MobileNotePropertyPatch } from './mobileNoteProperties'
|
||||
import { useMobileGitSyncFlow } from './useMobileGitSyncFlow'
|
||||
import { createNativeMobileGitTransport } from './mobileNativeGitTransport'
|
||||
import { loadExpoMobileGitNativeModule } from './mobileExpoNativeGitModule'
|
||||
|
||||
export function MobileApp() {
|
||||
const { width } = useWindowDimensions()
|
||||
@@ -73,7 +74,7 @@ export function MobileApp() {
|
||||
const showsProperties = width >= 1000
|
||||
const appStateStorage = useMemo(() => createNativeMobileAppStateStorage(), [])
|
||||
const gitCredentialStorage = useMemo(() => createNativeMobileGitCredentialStorage(), [])
|
||||
const gitTransport = useMemo(() => createNativeMobileGitTransport(), [])
|
||||
const gitTransport = useMemo(() => createNativeMobileGitTransport(loadExpoMobileGitNativeModule()), [])
|
||||
const gitHubOAuthClientIdState = useMemo(() => currentMobileGitHubOAuthClientIdState(), [])
|
||||
const vaultMetadataStorage = useMemo(() => createNativeMobileVaultMetadataStorage(), [])
|
||||
const [activeVaultMetadata, setActiveVaultMetadata] = useState(defaultMobileVaultMetadata)
|
||||
|
||||
6
apps/mobile/src/mobileExpoNativeGitModule.ts
Normal file
6
apps/mobile/src/mobileExpoNativeGitModule.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { requireOptionalNativeModule } from 'expo-modules-core'
|
||||
import { loadMobileGitNativeModule } from './mobileNativeGitModule'
|
||||
|
||||
export function loadExpoMobileGitNativeModule() {
|
||||
return loadMobileGitNativeModule({ loadModule: requireOptionalNativeModule })
|
||||
}
|
||||
29
apps/mobile/src/mobileNativeGitModule.test.ts
Normal file
29
apps/mobile/src/mobileNativeGitModule.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
loadMobileGitNativeModule,
|
||||
mobileGitNativeModuleName,
|
||||
type MobileGitNativeModuleLoader,
|
||||
} from './mobileNativeGitModule'
|
||||
import type { MobileGitNativeModule } from './mobileNativeGitTransport'
|
||||
|
||||
describe('loadMobileGitNativeModule', () => {
|
||||
it('loads the Tolaria Git native module when both operations exist', () => {
|
||||
const nativeModule: MobileGitNativeModule = {
|
||||
pull: vi.fn(),
|
||||
push: vi.fn(),
|
||||
}
|
||||
const loadModule = nativeModuleLoader(nativeModule)
|
||||
|
||||
expect(loadMobileGitNativeModule({ loadModule })).toBe(nativeModule)
|
||||
})
|
||||
|
||||
it('returns null when the native module is missing or incomplete', () => {
|
||||
expect(loadMobileGitNativeModule({ loadModule: () => null })).toBeNull()
|
||||
expect(loadMobileGitNativeModule({ loadModule: nativeModuleLoader({ pull: vi.fn() }) })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
function nativeModuleLoader(module: unknown): MobileGitNativeModuleLoader {
|
||||
return <ModuleType,>(moduleName: string) =>
|
||||
moduleName === mobileGitNativeModuleName ? module as ModuleType : null
|
||||
}
|
||||
34
apps/mobile/src/mobileNativeGitModule.ts
Normal file
34
apps/mobile/src/mobileNativeGitModule.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { MobileGitNativeModule } from './mobileNativeGitTransport'
|
||||
|
||||
export const mobileGitNativeModuleName = 'TolariaGit'
|
||||
|
||||
export type MobileGitNativeModuleLoader = <ModuleType>(
|
||||
moduleName: string
|
||||
) => ModuleType | null
|
||||
|
||||
export function loadMobileGitNativeModule({
|
||||
loadModule,
|
||||
}: {
|
||||
loadModule: MobileGitNativeModuleLoader
|
||||
}) {
|
||||
return asMobileGitNativeModule(
|
||||
loadModule<Partial<MobileGitNativeModule>>(mobileGitNativeModuleName),
|
||||
)
|
||||
}
|
||||
|
||||
function asMobileGitNativeModule(module: Partial<MobileGitNativeModule> | null) {
|
||||
return hasNativeGitOperation({ module, operation: 'pull' })
|
||||
&& hasNativeGitOperation({ module, operation: 'push' })
|
||||
? module as MobileGitNativeModule
|
||||
: null
|
||||
}
|
||||
|
||||
function hasNativeGitOperation({
|
||||
module,
|
||||
operation,
|
||||
}: {
|
||||
module: Partial<MobileGitNativeModule> | null
|
||||
operation: keyof MobileGitNativeModule
|
||||
}) {
|
||||
return typeof module?.[operation] === 'function'
|
||||
}
|
||||
@@ -466,7 +466,7 @@ interface PulseCommit {
|
||||
|
||||
The mobile app routes Git pull/push through `MobileGitTransport`. React Native UI and sync state code depend only on this TypeScript contract; the native implementation is introduced behind `createNativeMobileGitTransport`.
|
||||
|
||||
Until a native module is wired into Expo development builds, the native transport adapter returns a clear unavailable-module failure. This keeps authentication, sync status, retry UI, and future libgit2/Rust work testable without pretending that a JavaScript fallback is production Git.
|
||||
`createNativeMobileGitTransport` resolves the optional Expo native module named `TolariaGit` through `expo-modules-core`. Until that module is wired into Expo development builds, the native transport adapter returns a clear unavailable-module failure. This keeps authentication, sync status, retry UI, and future libgit2/Rust work testable without pretending that a JavaScript fallback is production Git.
|
||||
|
||||
The native boundary request is intentionally narrow for the first spike: active vault id, app-managed vault directory name, and remote URL. Credentials stay behind SecureStore/OAuth/native Git credential callbacks, and remote URLs must not embed tokens.
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Added the app-managed vault directory name to the native Git transport request so the future native module can locate the repository without deriving paths from display names.
|
||||
- Added TenTap horizontal-rule serialization so common divider output persists as canonical Markdown instead of blocking the draft.
|
||||
- Centralized mobile editor HTML entity decoding and added numeric entity / non-breaking-space support for paragraph, code, image/link, and table serialization.
|
||||
- Added the optional Expo native-module resolver for `TolariaGit`, so the JavaScript Git transport automatically binds to future development builds that include the native module and keeps the current unavailable-module failure everywhere else.
|
||||
- Created [ADR-0115](./adr/0115-expo-native-module-boundary-for-mobile-git.md) for the mobile Git native module discovery boundary.
|
||||
|
||||
## Next Action
|
||||
|
||||
@@ -396,6 +398,11 @@ Continue Phase 4 with editor durability:
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after centralized mobile HTML entity decoding.
|
||||
- CodeScene after centralized mobile HTML entity decoding: `apps/mobile/src/mobileHtmlEntities.ts`, `apps/mobile/src/mobileHtmlEntities.test.ts`, `apps/mobile/src/mobileEditorHtmlMarkdown.ts`, `apps/mobile/src/mobileEditorTableMarkdown.ts`, and `apps/mobile/src/mobileEditorDraft.test.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after centralized mobile HTML entity decoding.
|
||||
- `pnpm --filter @tolaria/mobile test -- src/mobileNativeGitModule.test.ts src/mobileNativeGitTransport.test.ts` passed after adding the optional `TolariaGit` native module resolver: 46 files / 152 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after adding the optional `TolariaGit` native module resolver.
|
||||
- CodeScene after adding the optional `TolariaGit` native module resolver: `apps/mobile/src/mobileNativeGitModule.ts`, `apps/mobile/src/mobileNativeGitModule.test.ts`, `apps/mobile/src/mobileExpoNativeGitModule.ts`, `apps/mobile/src/mobileNativeGitTransport.ts`, and `apps/mobile/src/MobileApp.tsx` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after adding the optional `TolariaGit` native module resolver.
|
||||
- `pnpm test -- src/App.test.tsx --silent` passed after extending the flaky desktop Git setup dialog wait that was blocking verified mobile commits in the full hook: 34 tests.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
31
docs/adr/0115-expo-native-module-boundary-for-mobile-git.md
Normal file
31
docs/adr/0115-expo-native-module-boundary-for-mobile-git.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# ADR-0115: Expo Native Module Boundary for Mobile Git
|
||||
|
||||
Date: 2026-05-05
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Tolaria mobile needs a real Git implementation inside the iOS and later Android app. The JavaScript UI already has app-local vault storage, Git credential state, GitHub OAuth, and a `MobileGitTransport` contract, but pull/push cannot be implemented safely in JavaScript alone.
|
||||
|
||||
The mobile strategy keeps over-the-air JavaScript updates important, but native dependencies still require development-client, TestFlight, or App Store builds. Git is one of those native capabilities: the JavaScript layer should expose a narrow, stable boundary while native code handles repository operations and credentials.
|
||||
|
||||
Expo SDK 55 provides `expo-modules-core` and `requireOptionalNativeModule`, which lets JavaScript discover a native module when it exists and degrade cleanly when running in Expo Go or a build that does not contain the module.
|
||||
|
||||
## Decision
|
||||
|
||||
Use an Expo native module named `TolariaGit` as the JavaScript-facing native Git boundary.
|
||||
|
||||
The JavaScript app imports `expo-modules-core` directly and resolves `TolariaGit` through `requireOptionalNativeModule`. `createNativeMobileGitTransport` binds to the module when it exposes both `pull` and `push`; otherwise it returns the explicit unavailable-module failure already used by the sync UI.
|
||||
|
||||
The native request stays narrow: vault id, app-managed vault directory name, and remote URL. Credentials remain behind SecureStore/OAuth/native Git credential callbacks; remote URLs must not contain tokens.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Expo Go remains useful for UI/editor work, but Git transport requires a development-client or production build that includes `TolariaGit`.
|
||||
- OTA updates can change JavaScript sync planning, UI, retry behavior, and request shaping, but native Git implementation changes require a binary update.
|
||||
- The future iOS module can use Rust/libgit2 or Swift/libgit2 behind the same TypeScript contract.
|
||||
- Android can later implement the same `TolariaGit` module name and request/result shape.
|
||||
- Unit tests can validate module discovery and transport normalization without native build output.
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -291,6 +291,9 @@ importers:
|
||||
expo-file-system:
|
||||
specifier: 55.0.17
|
||||
version: 55.0.17(expo@55.0.20)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
|
||||
expo-modules-core:
|
||||
specifier: 55.0.24
|
||||
version: 55.0.24(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
expo-secure-store:
|
||||
specifier: ~55.0.13
|
||||
version: 55.0.13(expo@55.0.20)
|
||||
|
||||
@@ -1254,7 +1254,11 @@ describe('App', () => {
|
||||
|
||||
render(<App />)
|
||||
|
||||
expect(await screen.findByText('Enable Git for this vault?')).toBeInTheDocument()
|
||||
expect(await screen.findByText(
|
||||
'Enable Git for this vault?',
|
||||
{},
|
||||
{ timeout: SLOW_APP_READY_TIMEOUT_MS },
|
||||
)).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('status-vault-trigger'))
|
||||
fireEvent.click(screen.getByTestId('vault-menu-item-Git Vault'))
|
||||
|
||||
Reference in New Issue
Block a user