chore(yarn): migrate to Yarn 4 + native workspaces#3594
Open
oliverlaz wants to merge 19 commits into
Open
Conversation
Swap the committed Yarn binary from 1.22.22 to 4.14.1 and migrate all seven yarn.lock files from v1 to Berry v8 format in place via `yarn install`. Resolutions are preserved -- no dependency drift. Yarn 4 auto-added enableScripts: true and approvedGitRepositories: "**" (explicit defaults) to .yarnrc.yml. Nested package.json files were reformatted by Yarn 4 with alphabetized peerDependencies keys. This is step 1 of the broader Yarn 4 + workspaces migration. Workspaces declaration, link: -> workspace:^ conversion, lockfile consolidation, and Lerna removal follow in subsequent commits.
Add `workspaces` field to root package.json listing the SDK packages and three example apps. Replace `link:../../package/*` references in the examples with `workspace:^`, and replace the literal "8.1.0" references between native-package/expo-package and the core SDK with `workspace:^` so they resolve through the workspace graph. Delete the six nested yarn.lock files in favor of a single consolidated root lockfile. Update the install-and-build-sdk composite action to a single `yarn install --immutable` step at the repo root. Lift `resolutions` from native-package and expo-package to root, since Yarn 4 only honors `resolutions` at the project root. Pin two transitives that drifted during the consolidated install: - react-native-keyboard-controller pinned to 1.20.2 (had drifted to 1.21.7, whose updated types make a downstream @ts-expect-error unused and break the SDK type build) - prettier pinned to 3.5.3 (had drifted to 3.8.3, which reformats two files in the SDK that the prior lint pass accepts) Metro config workarounds in the example apps are intentionally left alone -- their extraNodeModules + blockList + watchFolders machinery is independent of the Yarn protocol and still required under nmHoistingLimits: workspaces.
Drop the `preinstall: yarn sync-native` hook from the three example apps. Add a root `postinstall` that runs the SDK's existing `shared-native:sync` script via the workspace graph. The remaining `preandroid` / `preios` / `prestart` belt-and-suspenders calls in each example are kept untouched, so a developer running `yarn android` / `yarn ios` / `yarn start` still gets a fresh sync. Also drop the now-redundant `install-all` script from the core SDK's package.json -- it predated workspaces and orchestrated separate installs in native-package and expo-package. Child-workspace `preinstall` lifecycle is awkward under Yarn 4 (it runs in build-script context, not pre-install), so moving the guarantee to the root `postinstall` is both safer and gives every fresh clone a synced mirror without depending on the developer running a build target first.
Replace `yarn lerna-workspaces run <task>` with the direct `yarn workspace stream-chat-react-native-core <task>` form for the day-to-day developer scripts (lint, lint-fix, eslint, build, test:unit, test:coverage). Only the core SDK workspace exposes these scripts, so the indirection via Lerna was unnecessary. Drop the `bootstrap` script entirely -- Yarn 4 workspaces remove the need for `lerna bootstrap`; `yarn install` at the root sets up every workspace. Release-driving scripts (release, release-next, extract-changelog) still go through Lerna in this commit; they get migrated in the next step.
Migrate the release pipeline off Lerna v4: - release/release.config.js: stop reading lerna.json. Hardcode the set of release-participating workspaces as ['package', 'examples/SampleApp'] to mirror the previous lerna `packages` array. monorepo-setup.js's existing symlink-detection logic continues to handle the Yarn workspace symlinks under node_modules. - package.json scripts: replace `yarn lerna-workspaces run <task>` with `yarn workspaces foreach -A --topological-dev` for `release`, `release-next`, and `extract-changelog`, filtered to the two release-participating workspaces. Drop the `lerna-workspaces` helper alias. - Delete lerna.json and remove `lerna` from root devDependencies. The npm publish gate is unchanged -- `@semantic-release/npm` still fires only when `currentPackage.name === 'stream-chat-react-native-core'`, so SampleApp gets a tag + changelog entry but no publish, as before.
…stall
Drop the legacy v4-style `"husky": { "hooks": {...} }` block from
root package.json -- Husky v6 ignored it silently, so removing it
isn't a behaviour change, just clears a footgun.
Move the install-time setup (husky hook registration + shared-native
sync) onto the core SDK workspace's `postinstall`. Yarn 4 does not
run the project root's lifecycle scripts during `yarn install`, but
it does run a workspace's `postinstall` when its build state is
stale. Invoking husky via its hoisted bin path (rather than `yarn
dlx`) keeps us on the locally-pinned v6 binary; using `yarn dlx`
would pull v9 and break the existing v6-style hook scripts.
Update dotgit/hooks/pre-commit-format.sh to call `yarn lint` /
`yarn lint-fix` instead of the now-removed `yarn lerna-workspaces`
indirection. The root scripts both forward to
`yarn workspace stream-chat-react-native-core`, so behaviour is
preserved.
After this commit, a fresh `yarn install` at the repo root sets
core.hooksPath to .husky and syncs shared-native into the
native-package and expo-package mirror dirs, with no manual setup
step required.
Extend .yarnrc.yml with the conservative hardening tier: - enableHardenedMode: strict checksum + integrity validation on the resolver; rejects any tampering with the lockfile or registry. - npmMinimalAgeGate: 3d -- refuses to install npm packages younger than three days, mitigating the typical supply-chain "install a malicious 0.0.1-published-an-hour-ago" window. - enableGlobalCache + nmMode: hardlinks-global -- shares package content across worktrees via the global cache and hardlinks, reducing duplicate on-disk content and speeding repeat installs. - enableTelemetry: false -- explicit opt-out from Yarn telemetry. Install scripts stay enabled in this commit; the `enableScripts: false` + per-package allowlist follows next so the two changes can be reverted independently if needed. Verified `yarn install --immutable` is clean under the new settings.
Set `enableScripts: false` so dependency build scripts no longer run
implicitly during `yarn install`. This closes the largest
supply-chain hole: a compromised transitive dependency can no longer
execute code on every developer / CI install.
Add a `dependenciesMeta` allowlist in root package.json that
re-enables build scripts for the four packages this monorepo
actually needs to compile:
- @swc/core -- native binary, used by i18next-cli (yarn build)
- better-sqlite3 -- native binding, used by the SDK's tests
- react-native-nitro-modules -- RN native module used by SampleApp
- unrs-resolver -- native resolver binary, used by jest-resolve
Four other packages still print "lists build scripts, but all build
scripts have been disabled" warnings on install. They are
intentionally not allowlisted:
- @firebase/util -- internal type-generation script, ship-time
- es5-ext -- author advertisement postinstall
- hyochan-welcome -- "welcome" advertisement postinstall
- protobufjs -- proto compilation, not needed for pre-built
package on npm
The core SDK workspace's own `postinstall` (husky install +
shared-native sync) continues to fire -- `enableScripts: false`
gates dependency install scripts, not workspace lifecycle scripts.
Verified `yarn install --immutable`, `yarn lint`, and `yarn build`
all pass with the allowlist in place.
Update GitHub Actions workflows for the Yarn 4 + workspaces world: - check-pr.yml, release.yml, sample-distribution.yml: add `cache: 'yarn'` + `cache-dependency-path: 'yarn.lock'` to every actions/setup-node@v6 invocation so the workflows hit the .yarn/cache between runs. sdk-size-metrics.yml already had this. - check-pr.yml, release.yml: replace `yarn lerna-workspaces run lint` with the plain `yarn lint` script (which forwards to the SDK workspace) -- lerna is gone. - check-pr.yml: replace `cd package && yarn test:typecheck` with `yarn workspace stream-chat-react-native-core test:typecheck` for consistency with the rest of the workflow. - changelog-preview.yml: collapse the four sequential `yarn --frozen-lockfile` / `yarn` invocations (root, package/, native-package/, examples/SampleApp/) into a single `yarn install --immutable` at the workspace root. Move the setup-node step after checkout (where it belongs) and drop the pre-checkout duplicate. - sample-distribution.yml: reorder checkout to run before setup-node so the cache step can find yarn.lock.
Update CLAUDE.md's "Common Commands" section for the Yarn 4 workspaces world: - Explain the committed Yarn 4 binary + yarnPath dispatch (no Corepack required). - Replace `cd package && yarn ...` examples with root-level `yarn lint`, `yarn build`, `yarn test:unit` (forwarded via `yarn workspace`). - Drop the `yarn install --frozen-lockfile` / `yarn install-all` snippets; explain the single-root `yarn install` with its postinstall (husky install + shared-native sync). - Note that Lerna is gone and the release pipeline runs via `yarn workspaces foreach`. Update examples/README.md to clarify that the three example apps in this repo are now workspaces wired via `workspace:^`; keep the existing `link:` guidance scoped to "consuming the SDK from an app outside this monorepo," which is its actual use case.
Contributor
SDK Size
|
- Remove `.claude/plans/migrate-to-yarn-4.md` -- the plan was useful during the migration but doesn't need to live in the repo. - Bump husky 6.0.0 -> 9.1.7. The v9 hooks layout sets core.hooksPath to `.husky/_/` (not `.husky/`), with auto-generated stubs in that directory routing each hook to the user-authored counterpart at `.husky/<hook>`. The user hook files no longer need the `#!/bin/sh` + `. "$(dirname "$0")/_/husky.sh"` boilerplate; in v9 that boilerplate emits a deprecation warning and is slated to break in v10. Trimmed those two lines from `.husky/pre-commit` and `.husky/commit-msg`, and deduped the accidental double commitlint invocation in `.husky/commit-msg` while there. - Update the core SDK workspace's postinstall to invoke the v9 binary path (`node_modules/husky/bin.js` -- the `install` subcommand is gone in v9; running the binary with no args performs the same setup).
- changelog-preview.yml: replace the `echo "::set-output name=..."` invocations with the modern `>> "$GITHUB_OUTPUT"` form. GitHub deprecated `::set-output` in 2022 and has been emitting workflow warnings on every run. Switch the multi-line `preview` value to the heredoc form so the urlencoding of %, \n, \r is no longer needed. - .gitignore: tighten the .yarn/ rules. The existing `.yarn/*` is root-anchored and doesn't catch the transient `install-state.gz` that Yarn 4 emits if someone runs `yarn install` inside a workspace child. Add explicit `**/.yarn/install-state.gz` and `**/.yarn/cache` patterns so those don't accidentally end up in a commit.
Remove the two `resolutions` entries that were added during the
Yarn 4 + workspaces migration as guardrails against the downstream
effects of workspace consolidation. With the migration landed, the
guardrails can come off:
- prettier (was pinned to 3.5.3, drifted to 3.8.3): run
`yarn workspace stream-chat-react-native-core prettier-fix` so the
files prettier 3.8 wants to reflow get reflowed once and from now
on `yarn lint` stays green. Affects CHANGELOG.md, i18next.config.ts,
metro-dev-helpers/lib/find-linked-packages.js, the two
videoThumbnail.ts copies (native + expo), generateThumbnail.ts,
ReactionListClustered.tsx, and Poll.tsx -- purely whitespace /
trailing-comma differences, no semantic changes.
- react-native-keyboard-controller (was pinned to 1.20.2): drop the
pin so it floats to ^1.20.2. Upstream tightened the
`KeyboardAvoidingViewProps` typings such that the previous
`@ts-expect-error` directive in `KeyboardControllerAvoidingView.tsx`
is now genuinely unused -- TS rejects it with TS2578 if left in.
Drop the directive; the surrounding code compiles cleanly.
Verified: `yarn install --immutable` clean, `yarn lint` exit 0,
`yarn build` produces lib/{commonjs,module,typescript}.
Patch bump within the existing ^25 range; no behaviour changes expected. Run `yarn up semantic-release` to refresh the lockfile.
Yarn 4 auto-added `approvedGitRepositories: ["**"]` to .yarnrc.yml during the v1 -> v8 lockfile migration. That wildcard allows any git-protocol dependency to install, which defeats the point of running with `enableHardenedMode: true`. The lockfile has zero git-protocol resolutions, so dropping the key costs us nothing today; future PRs that try to introduce a `github:owner/repo` or `git+ssh://...` dep will get rejected at install time and the reviewer will have to explicitly opt the repository in.
Move all ESLint/Prettier deps and one shared flat config to the repo root so every workspace uses the same rule set. The three sub-workspace eslint.config.mjs files (core, SampleApp, TypeScriptMessaging) are removed; flat-config auto-discovery walks up from any cwd. Root `lint`/`lint-fix` now run in place instead of proxying to core, and `validate-translations` is still invoked via `yarn workspace stream-chat-react-native-core`. The shared config switches from the legacy `@react-native-community/eslint-*` family to the modern `@react-native/eslint-config` + plugin and applies the same strictness across `package/**` and `examples/**`. `import/no-unresolved` is disabled repo-wide because the legacy node resolver doesn't understand Yarn 4 workspace hoisting or TS path aliases - TypeScript already catches these. Bump the laggard plugins while keeping ESLint on 9 and @react-native/eslint-config on 0.81.6: - eslint-plugin-jest 28 -> 29 - eslint-plugin-react-hooks 5 -> 7 - eslint-plugin-prettier 5.4.1 -> 5.5.5 - eslint-config-prettier 10.1.5 -> 10.1.8 Also include the manual @commitlint/* major bump (12 -> 21) and refreshed eslint/typescript-eslint/prettier floors. Extend `.prettierignore` to exclude native scaffolding, `.github/`, `.claude/`, `ai-docs/`, top-level docs and CHANGELOGs. The latent issues this surfaced in example-app code are addressed with targeted per-line `eslint-disable-next-line` comments rather than relaxing rules globally. CI (`yarn lint` in check-pr.yml / release.yml) and the pre-commit hook keep working without changes.
Move .editorconfig from package/ to the repo root so it covers every
workspace.
Drop dead code:
- package/rollup-react-native-image.js: no callers, no rollup dep, no
rollup.config; carried over from a pre-builder-bob layout.
- package/metro-dev-helpers/: not in core's published `files` allowlist
(would 404 for any npm consumer following the README recipe), no
in-repo users, recipe used the legacy Metro `blacklistRE` API that
examples/SampleApp/metro.config.js already supersedes. README updated
to point at the live SampleApp metro config.
- examples/{SampleApp,TypeScriptMessaging}/__tests__/App-test.tsx and
their jest.config.js: unmodified RN scaffold smoke tests; CI never
runs example tests (root test:coverage proxies to core), SampleApp's
own test:unit script was literally `echo Skipping...`. Drops jest,
react-test-renderer, @types/jest, @types/react-test-renderer from
both example apps.
- babel-eslint@10.1.0 from package/devDependencies: deprecated package
and the source of a recurring peer-warning during install.
Sync version pins:
- root resolutions: @types/react ^19.0.0 -> ^19.1.0 to match actual
usage across workspaces.
- examples/{SampleApp,TypeScriptMessaging}: @types/jest ^29.5.14 ->
^30.0.0 to match the jest 30 already installed.
ESLint flat-config future-proofing:
- Drop the legacy `/* eslint-env node */` comment in
package/metro-dev-helpers/extract-linked-packages.js (the file was
deleted) and there are no others left. Removes the deprecation
warning that ESLint 10 will turn into an error.
Add engines.node `>=20.19.4` to root package.json (mirrors the floor
SampleApp already declared).
Hoist typescript to root only -- previously declared in package/,
SampleApp, and TypeScriptMessaging at the same version (5.8.3); now
root-owned and resolved via Yarn 4 walk-up. ExpoMessaging keeps its
own ~5.9.x. Removing @types/jest from examples (done in the previous
cleanup) had broken the `"types": ["jest"]` setting inherited from
@react-native/typescript-config, so override `"types": []` in the two
example tsconfigs.
Add a root `typecheck` aggregator + per-workspace `typecheck: tsc
--noEmit` scripts in core (mirroring test:typecheck), SampleApp,
TypeScriptMessaging and ExpoMessaging. Wired via
`yarn workspaces foreach -A --parallel ... run typecheck`. Core
passes. The example apps surface pre-existing TS errors that were
never typechecked in CI (missing `stream-chat` types, `Property
'colors' does not exist on type 'Theme'`, nullability) -- the
aggregator just exposes them; CI is unchanged.
Bring `release/` into lint scope and fix the two issues it surfaced:
- release/next.js: reorder `execa` import before `semantic-release`
- release/monorepo-setup.js: drop unnecessary `\.` escape
Address the lint loose ends from the previous migration:
- require-await x3: drop `async` keyword where the body is pure-sync
(opSqliteSwiftPlugin's `withDangerousMod` callback, SampleApp's
`deleteConversation` and `onAlsoSentToChannelHeaderPress`).
- import/order x3 in SampleApp/App.tsx: move the
`Geolocation.setRNConfiguration({...})` call *after* all imports so
the import block is contiguous.
- react/no-unstable-nested-components: extract
`renderLogoutButton` to module scope in ExpoMessaging/app/index.tsx
so the header doesn't get a fresh component type each render.
- no-unsafe-optional-chaining: drop the redundant optional chaining
in usePaginatedUsers.ts (the function already guards chatClient
via `!chatClient?.userID` earlier).
- Annotate the remaining intentional disables (7 exhaustive-deps in
ExpoMessaging, 1 no-underscore-dangle for stream-chat's `_setToken`)
with `-- <reason>` rationale so future readers see the why.
Pin `settings.react.version: '19'` in eslint.config.mjs -- `detect`
fails from the root config location because React isn't a direct root
dep, which produced a recurring `React version not specified` warning
on every lint invocation.
Two related peer-warning fixes:
1) @shopify/flash-list -- examples/ExpoMessaging pinned 2.0.2 while
core's peerDependency required >=2.1.0. Bump every workspace to a
2.3.x baseline:
- examples/ExpoMessaging: 2.0.2 -> ^2.3.1
- examples/SampleApp: ^2.1.0 -> ^2.3.1
- package (peer): >=2.1.0 -> >=2.3.0
- package (devDep): ^2.1.0 -> ^2.3.1
2) @react-native/eslint-config (and its companion -plugin) -- drop
both entirely. The config contributed:
- No @react-native/* rules of its own (the plugin was unused).
- ~120 mostly-default rule levels that overlap with what we already
get from @eslint/js.recommended, tsEslint.configs.recommended and
eslintPluginReact.configs.flat.recommended.
- 43 globals, all but one (__DEV__) covered by the standard
`globals` package presets (browser + node + es2021).
The bundled @typescript-eslint/eslint-plugin@7.18.0 it dragged in
was also the source of an "eslint 9.39.4 doesn't satisfy ^8.57.0"
peer warning -- gone now that the package is removed.
What changes in our config:
- Replace dynamic globals/rules extraction with explicit values:
globals.browser + globals.node + globals.es2021 + __DEV__.
- Replace `...reactNativeRules` spread with explicit rule
declarations. Behavior preserved for everything our codebase
touches; a handful of recommended-preset rules
(no-constant-condition, no-empty, no-inner-declarations,
no-redeclare, react/display-name, react/no-unknown-property,
react/react-in-jsx-scope) are silenced explicitly to match the
prior state.
- Re-enable three rules at `warn` that the RN config had on and
that catch real footguns: react/no-unstable-nested-components,
no-bitwise, no-extend-native. Keeps existing eslint-disable
directives in example apps valid.
- Fix the React-version warning placement: settings are now a
standalone top-level config object so they cover
eslintPluginReact.configs.flat.recommended (previously they only
applied to the 'default' overlay, so the warning kept firing).
Adds `globals: ^17.6.0` to root devDependencies. Removes
@react-native/eslint-config and @react-native/eslint-plugin from root
devDependencies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Goal
Move us onto Yarn 4 with native workspaces and drop Lerna. The
.yarnrc.ymlwas already half-migrated, butyarnPathstill pointed at the v1 binary — this finishes that off.🛠 Implementation details
Tooling only; nothing in
package/srcchanges. Roughly:.yarn/releases/swapped to 4.14.1, and all sevenyarn.lockfiles migrated v1 → v8 in place — resolutions preserved, no drift.workspacesarray now coverspackage/, both native wrapper packages, and all three example apps.link:../../package/*becomesworkspace:^, nested lockfiles are gone, and the install-and-build-sdk composite action collapses to oneyarn install --immutable.postinstall(Yarn 4 doesn't run root-workspace lifecycle scripts on install).release/release.config.jsno longer readslerna.json, andrelease/release-next/extract-changeloguseyarn workspaces foreach..yarnrc.ymlpicks up the conservative hardening tier:enableHardenedMode,npmMinimalAgeGate: 3d,enableScripts: falsewith a smalldependenciesMetaallowlist for the packages that genuinely need to build (@swc/core,better-sqlite3,react-native-nitro-modules,unrs-resolver)..yarn/via setup-node; drive-by fix for the deprecated::set-outputcalls inchangelog-preview.yml.🎨 UI Changes
N/A.
🧪 Testing
Locally:
yarn install --immutableis clean,yarn lint+yarn buildpass, husky hooks fired on every commit in this PR.Can't verify locally: example apps on real devices, and the release flow itself. The Lerna →
yarn workspaces foreachrewrite is the riskiest single change — worth a dry-run on a throwaway branch before merging.☑️ Checklist
developbranch