Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changeset/android-picker-viewport-jump.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/auth-avatar-and-cancel.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/bible-card-version-picker-default.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/default-bible-version-id-export.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/export-component-prop-types.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/pin-deps-supply-chain.md

This file was deleted.

6 changes: 0 additions & 6 deletions .changeset/react-doctor-top-fixes.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/refactor-useeffect-anti-patterns.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/sdk-version-header-stamp.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/sheet-header-surface-parity.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/sheet-max-width-wide-screens.md

This file was deleted.

35 changes: 25 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }}

# `id-token: write` is required so NPM Trusted Publishing can mint a short-lived
# OIDC token at publish time. With trusted publishing configured on npm there's
# no long-lived NPM_TOKEN to rotate, and every published tarball carries a
# OIDC token at publish time. The handshake is pnpm's own: `changeset publish`
# shells out to `pnpm publish`, and pnpm 11 publishes to the registry over HTTP
# itself rather than delegating to the npm CLI, so the npm version on the runner
# has no bearing on auth. With trusted publishing configured on npm there's no
# long-lived NPM_TOKEN to rotate, and every published tarball carries a
# provenance attestation linking it to this exact workflow run.
permissions:
contents: write
Expand All @@ -30,11 +33,6 @@ jobs:

- uses: ./.github/actions/setup

# Trusted publishing requires npm >= 11.5. Pin to latest to avoid the
# OIDC handshake silently falling back to NPM_TOKEN on older npm.
- name: Update npm to latest version
run: npm install -g npm@latest

# No separate build step: `pnpm release` runs `turbo build` before
# `changeset publish`, so the build only happens on the publish path.
- name: Create Release Pull Request or Publish to NPM
Expand All @@ -50,12 +48,29 @@ jobs:
title: 'chore: version packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# A no-op under pnpm 11, kept as belt and braces. pnpm resolves its own
# settings from PNPM_CONFIG_* and ignores NPM_CONFIG_PROVENANCE, so
# what actually turns provenance on is pnpm's OIDC path auto-enabling
# it for public repos on GitHub Actions, which this repo is. That
# auto-detection is read from pnpm's source, not observed on a real
# run; no OIDC publish has ever executed for these package names.
# Until one has, keep this: it costs nothing if the inference is
# right, and silently loses attestations if it's wrong.
NPM_CONFIG_PROVENANCE: true
# Publishing auth comes solely from OIDC trusted publishing. We do not
# pass a long-lived NPM_TOKEN. An automation token bypasses 2FA, so a
# standing secret widens the attack surface for little gain. If a
# bootstrap publish needs it before trusted publishing is live, add
# NODE_AUTH_TOKEN here as a temporary fallback and remove it after.
# standing secret widens the attack surface for little gain.
#
# If a bootstrap publish needs one before trusted publishing is live,
# NODE_AUTH_TOKEN on its own is not enough: pnpm never reads that
# variable. It only reaches pnpm through an .npmrc, which
# actions/setup-node writes (and points NPM_CONFIG_USERCONFIG at) only
# when it is given a `registry-url`, and .github/actions/setup does not
# set one. A working fallback therefore needs both halves:
# 1. `registry-url: 'https://registry.npmjs.org'` on the setup-node
# step in .github/actions/setup
# 2. `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` here
# Remove both once trusted publishing is live.

- name: Publish summary
if: steps.changesets.outputs.published == 'true'
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Keep `apps/example/metro.config.js` minimal — just `getDefaultConfig(__dirname

## Exports

**UI** (`@youversion/platform-react-native-expo-ui`): `YouVersionProvider`, `BibleCard`, `BibleChapterPickerSheet`, `BibleReader`, `BibleReaderSettingsSheet`, `BibleTextView`, `BibleVersionPickerSheet`, `VerseOfTheDay`, `YouVersionAuthButton`, and the `DEFAULT_BIBLE_VERSION_ID` constant (3034, Berean Standard Bible)
**UI** (`@youversion/platform-react-native-expo-ui`): `YouVersionProvider`, `BibleCard`, `BibleChapterPickerSheet`, `BibleReader`, `BibleReaderSettingsSheet`, `BibleTextView`, `BibleVersionPickerSheet`, `VerseOfTheDay`, and `YouVersionAuthButton`

**Core** (`@youversion/platform-react-native-expo-core`): `YouVersionProvider` (installation id + optional auth), `useYouVersion`, `useYVAuth`, `mmkvStorage`, and auth types (`AuthConfig`, `AuthScope`, `YVUserInfo`)

Expand Down
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ The visible controls around reader content, including chapter navigation, versio
_Avoid_: Toolbar when referring to product behavior rather than the Web SDK component name

**Compiled Distribution**:
The published package ships a compiled `build/` output (`expo-module build`, plain `tsc` → JS + `.d.ts`), not raw source. `tsc` preserves the `'use dom'` directive and the Expo Metro plugin processes it from compiled files in `node_modules`, so Expo DOM Components work when installed from npm. In-repo dev still resolves TypeScript source directly (`main` → `src/`); `publishConfig` swaps to `build/` at publish time, applied by pnpm (`pnpm publish` and `pnpm pack`) but not by raw `npm publish`. See ADR 0011 (supersedes the earlier source-only model).
The published package ships a compiled `build/` output (`expo-module build`, plain `tsc` → JS + `.d.ts`), not raw source. `tsc` preserves the `'use dom'` directive and the Expo Metro plugin processes it from compiled files in `node_modules`, so Expo DOM Components work when installed from npm. In-repo dev still resolves TypeScript source directly (`main` → `src/`); `publishConfig` swaps to `build/` at publish time, applied by pnpm (`pnpm publish` and `pnpm pack`) but not by raw `npm publish`. The import surface is sealed to the root entry point via a root-only `exports` map (dev → `src/index.ts`, publish → `build/index.js`), so deep imports into internal files are not part of the API. See ADR 0011 (supersedes the earlier source-only model).
_Avoid_: Source-only, "a compiled build strips the directive"

**SDK Attribution Header**:
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ Native SDK strings follow the device locale by default; see the [localization gu
Display a verse range or single verse with `BibleTextView`:

```tsx
import { BibleTextView, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui'
import { BibleTextView } from '@youversion/platform-react-native-expo-ui'

function VerseScreen() {
return (
<BibleTextView
reference="JHN.3.16" // USFM reference: BOOK.CHAPTER.VERSE (or VERSE-VERSE for a range)
versionId={DEFAULT_BIBLE_VERSION_ID} // 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com
versionId={3034} // 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com
/>
)
}
Expand All @@ -117,11 +117,11 @@ function VerseScreen() {
Display a Bible card with a verse and reader controls:

```tsx
import { BibleCard, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui'
import { BibleCard } from '@youversion/platform-react-native-expo-ui'

// DEFAULT_BIBLE_VERSION_ID is 3034 (Berean Standard Bible); find other IDs at platform.youversion.com
// 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com
function CardScreen() {
return <BibleCard reference="JHN.3.16" defaultVersionId={DEFAULT_BIBLE_VERSION_ID} />
return <BibleCard reference="JHN.3.16" defaultVersionId={3034} />
Comment thread
cameronapak marked this conversation as resolved.
}
```

Expand All @@ -134,11 +134,11 @@ function CardScreen() {
`BibleReader` gives you a full Bible reading experience, ready to drop in as a tab or full screen:

```tsx
import { BibleReader, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui'
import { BibleReader } from '@youversion/platform-react-native-expo-ui'

// DEFAULT_BIBLE_VERSION_ID is 3034 (Berean Standard Bible); find other IDs at platform.youversion.com
// 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com
function ReaderScreen() {
return <BibleReader defaultVersionId={DEFAULT_BIBLE_VERSION_ID} />
return <BibleReader defaultVersionId={3034} />
}
```

Expand All @@ -150,7 +150,7 @@ To present your own picker UI instead of the built-in sheets, pass `onChapterPic

```tsx
<BibleReader
defaultVersionId={DEFAULT_BIBLE_VERSION_ID}
defaultVersionId={3034}
onVersionPickerPress={({ versionId, languageId }) => {
// present your own version picker
}}
Expand All @@ -162,11 +162,11 @@ The standalone sheets are also exported (`BibleChapterPickerSheet`, `BibleVersio
### Verse of the Day

```tsx
import { VerseOfTheDay, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui'
import { VerseOfTheDay } from '@youversion/platform-react-native-expo-ui'

// DEFAULT_BIBLE_VERSION_ID is 3034 (Berean Standard Bible); find other IDs at platform.youversion.com
// 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com
function VotdScreen() {
return <VerseOfTheDay versionId={DEFAULT_BIBLE_VERSION_ID} />
return <VerseOfTheDay versionId={3034} />
}
```

Expand Down
26 changes: 26 additions & 0 deletions docs/adr/0011-compiled-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ The build tool is [`expo-module-scripts`](https://www.npmjs.com/package/expo-mod
- `publishConfig` swaps `main` / `types` to `build/`, and `files: ["build"]` keeps source out of the tarball, so the published package ships compiled JS plus real `.d.ts`.
- Releases MUST go through `pnpm changeset publish` (the configured pipeline). `pnpm publish` applies the `publishConfig` field overrides; raw `npm publish` does not — `main` would stay `src/index.ts` while `files: ["build"]` excludes `src/`, so the tarball would be broken (entry point missing or unable to resolve its imports), not a clean source ship. See [PUBLISHING.md](../../PUBLISHING.md).

## Sealed import surface

Both packages declare a root-only `exports` map. The only entry points are `"."` (the package root) and `"./package.json"`; there are no subpath entries, so the supported way to consume either package is `import … from '@youversion/platform-react-native-expo-ui'` / `-core`, and a deep import like `@youversion/platform-react-native-expo-ui/build/dom/bible-card` is not part of the public API.

Why seal it:

- **Raw DOM components are not package API.** Only the barrel (`src/index.ts` → `build/index.js`) is public. DOM component files exist purely as the WebView-backed implementation of the native wrappers.
- **Deep imports would freeze the internal file layout.** Without `exports`, `build/dom/*`, `build/native/*`, and `build/lib/*` are all reachable, so any consumer importing them turns our directory structure into a compatibility contract. The map keeps refactors of internal layout non-breaking.

### Enforcement boundary (important)

The map is enforced strictly by Node's own resolver and by TypeScript, and loosely by Metro:

- **Node resolution** hard-errors on an unmatched subpath: `require.resolve('@youversion/platform-react-native-expo-ui/build/dom/bible-card')` throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. Same for any strict `exports`-aware bundler (webpack, Vite, esbuild, Rollup with default settings).
- **TypeScript** (`moduleResolution` `node16`/`nodenext`/`bundler`) refuses to resolve types for a deep path, so a consumer writing a deep import gets a compile error in-editor and in CI typecheck. This is the primary developer-facing guardrail — the import is rejected before it can ship.
- **Metro does _not_ enforce it.** Even with `resolver.unstable_enablePackageExports: true` (the Expo SDK 56 default), Metro falls back to plain filesystem resolution for a subpath that the `exports` map does not match, instead of hard-erroring the way Node does. Verified: a fresh SDK 56 app installing the packed tarballs bundles a deep import into `build/dom/*` without error. So at the RN bundler level a determined consumer can still reach internal files at runtime; the `exports` map does not make that physically impossible, it makes it unsupported and type-rejected. If we ever need Metro-level enforcement we would have to remove the files from the tarball or split internals into a separate unpublished package — out of scope here.

Net: the seal reliably stops deep imports from being _written_ (TypeScript) and from resolving under Node and standard web bundlers; it documents internal files as non-API; it does not physically block Metro's loose runtime fallback.

Dev vs. publish, same swap as `main`/`types`:

- Top-level `exports["."]` points at `./src/index.ts` (both `types` and `default`), so in-repo Metro and `tsc` resolve TypeScript source directly — no build step for local dev or the example app.
- `publishConfig.exports` overrides `"."` to `types: ./build/index.d.ts`, `default: ./build/index.js`, applied by `pnpm publish` exactly like the `main`/`types` overrides. `"./package.json"` stays exposed in both.

Timing: loosening an `exports` map later (adding subpaths) is additive and non-breaking, while tightening one later (removing subpaths consumers already import) is breaking. Sealing before the first real release (0.9.1) means the surface starts minimal and can only widen, never narrow, so no consumer ever depends on an internal path we later want to remove.

## Consequences

- Gains real `.d.ts` and removes 0002's `moduleResolution: bundler`-only constraint for consumers.
Expand Down
Loading