Skip to content

feat(repack): federation workspace tooling - #1466

Open
gedu wants to merge 19 commits into
callstack:mainfrom
gedu:feat/federation-shared-config
Open

gedu wants to merge 19 commits into
callstack:mainfrom
gedu:feat/federation-shared-config

Conversation

@gedu

@gedu gedu commented Sep 22, 2026

Copy link
Copy Markdown

The manifest taught us to see drift between host and remotes. This makes drift hard to create in the first place:

  • defineShared() builds the shared option with exact pins resolved from what is actually installed. No more version literals rotting in eight config files.
  • repack-federation.json is a committed map of the workspace (host, remotes, manifest paths), so doctor and init run with zero flags in any repo.
  • federation-doctor gains --dry-run (pre-build checks from package.json alone, no build needed) and opt-in --pairwise remote-vs-remote comparison.
  • federation-init scaffolds a new remote from a feature folder: scans the module graph, generates versionless configs built on defineShared, aligns versions with the host and asks before writing anything. --standalone is a runtime flag

The host-eager / remote-lazy convention now reports as EAGER_ADVISORY instead of erroring (the small commit from last week, already commented on #1463).

gedu added 19 commits September 21, 2026 11:48
…uild checks

- additive getSharedConfiguration() on ModuleFederationPluginV1/V2: returns
  the constructor shared option verbatim (pre deep-import injection)
- extractShared.ts: evaluate an app's bundler config in-process
  (loadProjectConfig + synthetic env), duck-type the plugin instance, and
  resolve its shared option into virtual manifest entries per app root
- dryRun.ts: runDryRun reuses the generalized checkSharedDeps codes over
  virtual entries; MISSING_SHARED_PROVIDER (warning) from the both-declared
  package.json heuristic; UNBUILT_CAVEAT suffix on every finding message so
  the locked --format json shape survives
- federation-doctor --dry-run wiring: no builds, no manifest fetches, no
  MISSING_REMOTE_MANIFEST; ConfigEvalError -> message + exit 2, never a
  stack; exit codes 0/1/2 identical across modes
- dry-run-workspace fixtures with per-app mini node_modules pins + CJS
  stub configs; .gitignore negations so fixture node_modules get committed
- federation-doctor.mdx: --dry-run option docs

Focused: jest -- extractShared dryRun federationDoctor doctor => 83/83.
Full: 49 suites / 520 tests; pnpm typecheck, pnpm lint:ci clean.
…oling-side refusal

- --standalone boolean flag on start and bundle commands
- EnvOptions.argv (new additive public type): getEnvOptions always sets
  argv = { standalone: <flag> }, so configs read env.argv?.standalone
  safely; reaches defineRspackConfig/defineWebpackConfig evaluation
  through the same { ...env, platform } spread as --platform — no second
  injection point, nothing ever persisted
- assertStandaloneSupported in configFile.ts: refuses only when
  repack-federation.json exists AND declares the matching remote entry
  without standalone: true (standalone: false = unsupported); no file or
  unmatched root proceeds; malformed file refuses as CLIError, no stack
- start/bundle call the guard up front when --standalone is requested —
  refusal is tooling-side only, the bundler runtime never reads the
  workspace map
- docs: --standalone sections on start/bundle CLI pages

Focused: jest -- getEnvOptions configFile options standalone => 53/53.
Full: 50 suites / 536 tests; pnpm typecheck, pnpm lint:ci clean.
…dvisories

- scanFeatures.ts: regex/state-machine scanner (no babel, zero new deps)
  returning { dependencies, advisories }; a code-state walk masks comments
  and string/template content (escape-tracked), collects static
  import-from / bare import / export-from / require('lit') specifiers,
  maps subpaths to package roots, ignores react/jsx-runtime noise, filters
  relative and @/-alias imports, skips __tests__/ and *.test.* files and
  unsupported extensions
- every dynamic pattern (import(), computed/template require) yields an
  honesty advisory naming file:line and stating the set is NOT exhaustive
  — manifest dynamicImportDetected semantics, never a silent pass
- features-store fixtures cover static imports, scoped packages, requires,
  dynamic template-literal requires, commented/fake imports in strings and
  comments, relative imports and test-file skips
- biome/tsconfig exclusions for src/commands/federation/__fixtures__:
  fixture sources are input data with fake imports and line-significant
  formatting — never linted, formatted or typechecked
  (features-store/__tests__ uses .jsx so jest testMatch never collects it)

Focused: jest -- scanFeatures => 11/11.
Full: 51 suites / 547 tests; pnpm typecheck, pnpm lint:ci clean.
…e-write) with prompts

computeInitPlan computes everything federation-init would write without
writing it: key-level merges over package.json / repack-federation.json /
host remotes surgery (anchored, manual-steps degradation), create-only
versionless rspack+webpack configs consuming defineShared with the remote
role and env.argv-derived mode, host-vs-remote divergence and --yes
pin-rewrite plans. applyPlan writes exactly plan.files; prompt helpers
(apply [y/N], divergence [a]lign/[i]gnore/[c]ancel) take an injected ask.

Evidence:
- Focused: pnpm --filter @callstack/repack test -- initPlan initApply diff
  -> 40 passed (3 suites); full suite 54 suites / 587 tests green.
- Gates: pnpm typecheck and pnpm lint:ci clean on this tree.
- Runtime harness: N/A at unit level — pure plan/merge/diff engine; the
  real end-to-end run lands with the flat command into a scratch copy of
  tester-federation (task 10.1 harness).
- Rollback: revert this commit — init/{plan,merge,templates,diff,apply,
  prompt}.ts, the three suites and the init-workspace fixtures are new
  files; nothing imports them yet (scanner untouched, no command wired).
react-native federation-init <feature-folder> --name <remote> composes
scan -> computeInitPlan -> formatPlanDiff -> prompt gate -> apply on the
workspace resolved from repack-federation.json. Command-level refusals
are exit 2 with clear messages and zero writes: missing --name/feature
folder, no/malformed workspace config, --standalone on a targeted remote
that does not declare it (assertStandaloneSupported), and manual steps
blocking --yes. Interactive runs show diffs first, then [a/i/c] on
divergence and apply [y/N]; --yes auto-aligns and reports each
pkg: old -> new pin rewrite.

Evidence:
- Focused: pnpm --filter @callstack/repack test -- index options
  federationInit -> 37 passed (5 suites); full suite 55 suites /
  602 tests green. pnpm typecheck and pnpm lint:ci clean.
- Runtime harness: ran the built dist command outside jest
  (node -e require('dist/commands/federationInit.js')) against a scratch
  copy of the init-workspace workspace: printed the full plan diff,
  applied 6 files, and reported divergence + alignment
  ('remote-drift: react: ^9.8.7 -> 9.9.9' + package-manager-install
  instruction). tester-federation end-to-end lands with 11.2: today's
  tester app has no repack-federation.json (11.1 creates it), keeps
  configs under configs/ (outside init's default discovery), and init
  has no --config passthrough by design.
- Rollback: revert this commit — removes the command entry, options,
  args type and doc page; the init/ engine files become inert (no
  other importer).
Replace hardcoded 19.2.3/0.84.1 pins, pkg.dependencies lookups and
react(-native)/package.json proto-defineShared imports in all 8
tester-federation and tester-federation-v2 rspack/webpack configs with
Repack.defineShared(SHARED_DEPS, { context, role, mode }): host configs
role 'host' (eager), mini configs role 'remote' with the runtime-only
mode line via env.argv?.standalone. Exact pins now resolve from the
installed workspace — the old committed literals had drifted (react
19.2.3 vs installed 19.2.8, react-native 0.84.1 vs installed 0.86.0).

Add apps/tester-federation/repack-federation.json as the demo workspace
map (host root+manifest, MiniApp manifest/root/standalone/port) driving
zero-flag federation-doctor.

E2E smoke evidence (real rspack 1.6.0 + webpack 5.105.4 builds, ios):
- host/mini/v2-mini builds: compiled successfully, exit 0 (final configs)
- USE_WEBPACK=1 --bundler webpack host build: compiled successfully
- zero-flag federation-doctor from apps/tester-federation (config file
  drives it): 0 errors, 7 EAGER_ADVISORY warnings, exit 0 — the
  host-eager/remote-lazy pair reports as advisory, not EAGER_MISMATCH
- doctor --pairwise across both mini manifests: 0 errors, exit 0
- standalone flip: react-native webpack-bundle ... --standalone on mini
  -> manifest shows all shared eager=true, git status shows no tracked
  file changed (runtime-only via env.argv, nothing committed)
- manifests agree on every exact pin (doctor reports zero version drift)
…figs

Two defects surfaced by the 11.2 e2e smoke on real tester builds:

1. react-native federation-manifest/federation-init crashed under
   @react-native-community/cli >= 17 (commander wiring): positionals not
   declared in the command name never reach func — argv[0] carries the
   parsed options object, so path.resolve threw ERR_INVALID_ARG_TYPE even
   with --source given. Fix: declare optional positionals in the command
   names ('federation-manifest [source]', 'federation-init
   [feature-folder]') and trust only string argv[0], falling back to
   parsed args otherwise. Reproduced live pre-fix, verified post-fix from
   apps/tester-federation and a scratch init dogfood workspace.

2. federation-doctor --dry-run could not read the shared setup of remotes
   scaffolded by federation-init, which generates rspack.<remote>.mts —
   not a conventional rspack.config.* name. extractAppShared now falls
   back to a tooling-style rspack.<name>.<ext> config when it is the only
   rspack-prefixed candidate (webpack second, ambiguity never guessed).

Strict TDD: RED 1 failing discovery test + 3 new command/registry tests
failing for the wiring reason; GREEN 85/85 focused, full 55 suites/607
tests, typecheck, lint:ci. Live post-fix: zero-flag doctor exit 0 with
EAGER_ADVISORY only; --dry-run on an init-scaffolded scratch workspace
exit 0 (host via conventional name, remote via the new fallback); real
CLI 20 federation-init applied 5 files, idempotent re-run 'Nothing to do'
…ections

- website: new docs/features/federation-workspace.md tying defineShared,
  repack-federation.json, federation-doctor (zero-flag/--dry-run/
  --pairwise/EAGER_ADVISORY) and federation-init into one workflow guide,
  wired into the features _meta.
- agent_context/federation-tools/design.md: PR 4 command-surface
  correction — the init codemod ships as the flat `react-native
  federation-init` command (no repack bin, no subcommand tree, no
  shared.config.ts convention) — plus as-built notes for defineShared,
  the workspace-map schema, doctor extensions, init behavior, and the
  RN CLI >= 17 positional-declaration contract.

API doc completeness verified (not re-edited): define-shared.md,
repack-federation-json.mdx, federation-init.mdx and federation-doctor.mdx
already cover --dry-run/--pairwise/advisory semantics and --standalone
(bundle/start pages).
@gedu
gedu requested a review from dannyhw September 22, 2026 08:53
@gedu gedu self-assigned this Sep 22, 2026
@changeset-bot

changeset-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6b6758f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@callstack/repack Minor
@callstack/repack-plugin-expo-modules Minor
@callstack/repack-plugin-nativewind Minor
@callstack/repack-plugin-reanimated Minor
@callstack/repack-dev-server Minor
@callstack/repack-init Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

@gedu is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant