Skip to content

Latest commit

 

History

History
261 lines (210 loc) · 13.9 KB

CONTRIBUTING.md

File metadata and controls

261 lines (210 loc) · 13.9 KB

Contributing

Thanks for contributing to @comapeo/core-react-native. This guide covers the development setup, how to run the tests, and the commit/PR/release conventions.

For installation and usage of the published module see the README. For the architecture, process model, and directory-by-directory breakdown see AGENTS.md.

Prerequisites

  • Node.js 24 (see .nvmrc; fnm use or nvm use picks it up).
  • For iOS work: macOS with Xcode. For Android work: Android Studio / SDK + NDK.
  • For local end-to-end runs: Maestro (curl -fsSL "https://get.maestro.mobile.dev" | bash) and Java 17+.
npm install        # install module deps and build the module (prepare)
npm run setup      # fetch nodejs-mobile, build the backend, install the test apps

npm run setup runs, in order:

  • download:nodejs-mobile — pulls NodeMobile.xcframework (iOS) and libnode.so per ABI plus headers (Android) into place; these are not committed. It fetches the lite runtime flavour, which drops ICU, the inspector, node:sqlite and TypeScript type-stripping — none of which we use. NODEJS_MOBILE_FLAVOR=full fetches the full one; the two ship identical headers, so addon prebuilds work against either.
  • backend:build — bundles the Node.js backend (backend/) that gets embedded in the app. npm install alone does not build it.
  • installs dependencies for the two test apps (apps/integration, apps/e2e).

Re-run npm run setup after pulling changes that bump the nodejs-mobile version or touch the backend. The individual steps are also available on their own (npm run download:nodejs-mobile, npm run backend:build).

Backend sourcemaps

backend:build emits one bundle per platform, with its sourcemap relocated to a sibling nodejs-sourcemaps/ directory so it never enters the APK/IPA. Symbolication is server-side: @sentry/rollup-plugin embeds a content-derived debug ID in the bundle, the consuming app uploads the maps with comapeo-rn-upload-sourcemaps, and Sentry matches them by that ID.

The backend deliberately does not run with --enable-source-maps in any variant. Measured on the Node 18 nodejs-mobile used to pin, findSourceMap() re-parsed the whole map on every Error.stack format — roughly 320 ms and 250–470 MB of garbage per error for our 19 MB map, enough to wedge the event loop for tens of seconds on a low-end device. Node has since reworked its source-map cache; the flag stays off until that's re-measured on device. For a stack you have in a terminal rather than in Sentry, comapeo-rn-symbolicate remaps it offline from the shipped maps.

Repository layout

This is a single published npm package with its build tooling and test apps in the same repo. The short version (full detail in AGENTS.md):

  • src/, android/, ios/ — the published module.
  • backend/ — the Node.js backend rolled up by scripts/build-backend.ts and embedded in the app.
  • apps/integration/ — Expo app used as the integration-test harness (it also runs as a dev app). Not published. Its ios/ and android/ trees are gitignored and regenerated by expo prebuild; the source-of-truth test files live under apps/integration/tests/ and are re-injected by the app-only config plugins.
  • apps/e2e/ + maestro/e2e.yaml — the end-to-end test app and its Maestro flow (run on BrowserStack in CI).
  • scripts/ — build tooling and the local Android test runner (run-instrumented-tests.sh).

Development

Module and backend:

npm run build              # compile the module's TypeScript (expo-module build)
npm run clean              # remove build artifacts
npm run lint               # ESLint
npm run backend:build      # bundle the embedded Node.js backend (backend/)
npm run download:nodejs-mobile   # re-fetch the nodejs-mobile runtime + headers
npm run open:ios           # open the integration app in Xcode
npm run open:android       # open the integration app in Android Studio

Building the test apps. Each app's build:* script runs expo prebuild then a Release build onto a connected simulator/emulator (after npm run setup has installed its deps and built the embedded backend in via the config plugin):

npm run e2e:ios                                  # e2e, iOS Release build + install (alias of apps/e2e build:ios)
npm run e2e:android                              # e2e, Android Release build + install

# the underlying per-app Release builds (each app has both):
npm --prefix apps/e2e run build:ios              # e2e, iOS Release
npm --prefix apps/e2e run build:android          # e2e, Android Release
npm --prefix apps/integration run build:ios      # integration, iOS Release
npm --prefix apps/integration run build:android  # integration, Android Release

build:ios (expo run:ios --configuration Release) and build:android (expo run:android --variant release) build the Release configuration onto a connected simulator/emulator — what the e2e suite drives locally (see below), and handy for release-only behaviour and FGS cold-start timing (debug is ~10× slower). They are not the BrowserStack artifacts: CI builds those separately in e2e-reusable.yml — Android via gradlew assembleRelease (an APK) and iOS via xcodebuild archive (an unsigned arm64 device .ipa that can't run on a simulator at all).

The e2e app imports @comapeo/core-react-native (this repo) through a node_modules/@comapeo/core-react-native symlink to the repo root — deliberately not a tsconfig paths alias, which would make Metro load the module twice and collide the two IPC singletons' RPC message IDs. apps/e2e/scripts/link-local-module.js creates that symlink idempotently: metro.config.js calls it when Metro loads, and the CI typecheck step runs it directly, because tsc never loads Metro and expo prebuild doesn't create the link either. If you get Cannot find module '@comapeo/core-react-native…' while typechecking the e2e app, run node apps/e2e/scripts/link-local-module.js first.

For a plain dev/demo run with Metro and fast JS reload (not e2e — see §"End-to-end locally" for why a dev build doesn't help there), use an app's own ios / android / start scripts, e.g. npm --prefix apps/integration run ios.

Tests

Run npm run lint and npm run test before opening a PR.

How the suites, workflows, and CI gating fit together (the merge queue, the e2e trust boundary, why some checks run where they do) is documented in docs/TESTING.md. This section is the short how-to.

Each of the seven test layers (see docs/TESTING.md §2 for what each one verifies) has a root script:

Layer Command Notes
JS lint npm run lint ESLint
JS unit npm run test TypeScript unit tests (src/__tests__/)
Backend unit npm run backend:test Node --test suite in backend/
Swift package npm run test:swift macOS, no simulator (cd ios && swift test)
JVM unit npm run test:android:unit no device
Android instrumented npm run test:android needs a booted emulator / device
iOS integration npm run test:ios needs the integration app prebuilt + a simulator
End-to-end (local) npm run e2e:ios / e2e:android, then npm run e2e:test Release build + Maestro; see below

The native/integration commands are slower and platform-specific. The CI workflows remain the source of truth for the exact flags and device matrix: ios-tests.yml, android-tests.yml, e2e-tests.yml. test:android / test:android:unit wrap scripts/run-instrumented-tests.sh — call the script directly to pass --class <TestClass> or --skip-build.

End-to-end locally

The e2e app runs its in-app suite on a local simulator/emulator — including iOS. Locally you drive a Release build with the exact maestro/e2e.yaml flow CI runs on BrowserStack:

npm run e2e:ios          # or: npm run e2e:android — Release build + install onto a sim/emulator
npm run e2e:test         # drive it with Maestro (maestro/e2e.yaml)

A Release build disables the dev menu / LogBox overlays and loads its embedded JS bundle (no Metro), so the local run needs no dev-build workarounds and matches what CI tests. An expo run:ios Release build signs with your Apple development team, so the embedded Node backend's keychain access works. (CI instead runs iOS e2e on real BrowserStack devices from an unsigned device archive — a device binary that can't run on a simulator regardless of signing, so don't try to run the BrowserStack artifact locally.)

There is deliberately no dev-client e2e variant. A dev build's only advantage is fast JS reload, but nearly all of this module is native + nodejs-mobile code that needs a full rebuild on change anyway — so the dev-build path buys nothing here, and the Release build is the one worth testing.

For writing and debugging Maestro flows see the bundled maestro-mobile-e2e skill.

The native suites and the merge queue

Merges go through a required merge queue: a PR's required checks are evaluated on the queued merge commit before it lands, so the actual merged result is what gates the merge, and several PRs can land without each one re-running CI every time another merges.

The fast suites (JVM unit tests, Swift package tests, lint) run on every PR. The heavy suites — Android instrumented, iOS integration + device build, and the BrowserStack e2e — are slow/paid, so by default they run only in the merge queue, not on every PR push: a plain PR gets cheap green checks and the real runs happen when the PR is queued. To run the heavy suites on a PR before queueing it (e.g. you touched device-facing code), add the run-e2e label or trigger the workflow manually. See docs/TESTING.md §5 for the full decision table.

The BrowserStack e2e needs secrets, which GitHub withholds from Dependabot and fork PRs (so an untrusted dependency can't read them). Those PRs build but skip the device run until a maintainer reviews the diff — including the Socket.dev supply-chain report — and adds the safe-to-test label, which triggers the isolated .github/workflows/e2e-trusted.yml. The label is removed after the run and on every new commit, so a Dependabot bump can never re-run secret-bearing CI without a fresh review.

Pull requests and commit conventions

PRs are merged with a merge commit. The release notes are generated from merged PRs, so the PR title drives the changelog (not the individual commits). Titles must follow Conventional Commits and are linted by .github/workflows/pr-title.yml:

<type>(<optional scope>): <subject>

Allowed types: feat, fix, perf, docs, refactor, test, chore, ci, build. Use chore(deps): … for dependency bumps.

The workflow reads the type/scope and applies one changelog label, which .github/release.yml groups into the release notes:

Title Changelog section
feat: … Features
fix: … Bug Fixes
perf: … Performance
docs: … Documentation
refactor / test / chore / ci / build Maintenance
any …(deps): … Dependencies
feat!: … or a BREAKING CHANGE: footer in the body Breaking Changes

The label is derived from the title automatically — don't set the changelog labels by hand; editing the title updates them. Add the skip-changelog label to leave a PR out of the release notes entirely.

Dependency updates are opened by Dependabot (.github/dependabot.yml) with a 3-day cooldown, so nothing newer than 72 hours since publish is pulled in.

Releases

Releases are cut by the release workflow (.github/workflows/release.yml), which wraps optic-release-automation-action:

  1. Run the Sentry release smoke test — a ~10-minute manual checklist plus the scripted API tripwire (scripts/sentry-tripwire.mjs) that verify telemetry actually reaches Sentry from a test build.
  2. Trigger the workflow manually (workflow_dispatch) and pick the semver bump (auto, patch, minor, major, or a pre* prerelease). This opens a release PR with the generated changelog.
  3. Merging that PR builds and publishes to npm. Publishing runs on macOS because the tarball ships the per-addon iOS xcframeworks, which need Xcode tooling to assemble.

Prereleases publish under the pre npm tag from -pre. branches; stable releases publish under latest from main.