Skip to content

Fixed react-world-flags interop so shade's Flag renders under both bundlers#29137

Closed
9larsons wants to merge 1 commit into
mainfrom
steve/pla-96-a-react-world-flags-interop
Closed

Fixed react-world-flags interop so shade's Flag renders under both bundlers#29137
9larsons wants to merge 1 commit into
mainfrom
steve/pla-96-a-react-world-flags-interop

Conversation

@9larsons

@9larsons 9larsons commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

ref https://linear.app/ghost/issue/PLA-96/spike-rolldown-vite-drop-in-build-benchmarks

What

Shade's Flag component imports react-world-flags, a CommonJS module shaped { __esModule: true, default: <Component> }. It replaces the bare default import with a namespace import plus an explicit interopDefault() unwrap.

Why

esbuild and Rollup unwrap the CJS default down to the component automatically, so <ReactFlag/> works today. The Rolldown bundler (being evaluated for the admin apps under PLA-96) instead hands back the __esModule wrapper object — whose .default is still the wrapper — so <ReactFlag/> renders an object and React throws Element type is invalid: got object, crashing the analytics location/source views that use Flag.

The fix

interopDefault() reads .default off the module (namespace or CJS export), and if that value is itself still an __esModule-tagged wrapper, unwraps one more level. This resolves to the actual component under both toolchains:

  • Rollup (current): behavior-preserving — the first .default yields the component, second branch is a no-op.
  • Rolldown (target): the extra unwrap peels the wrapper object down to the component.

Scope

  • Single file: apps/shade/src/components/ui/flag.tsx. The <ReactFlag .../> JSX usage is unchanged.
  • Workstream A / Phase-0 of the Rolldown migration (PLA-96) — de-risking landed under the current working toolchain so the Rolldown eval isn't blocked by this crash.

Verification (Rollup toolchain)

  • pnpm --filter @tryghost/shade build — clean (includes tsc + tsc-alias + vite build)
  • pnpm --filter @tryghost/shade lint — clean
  • pnpm --filter @tryghost/shade test:unit — 226 passed (includes tsc --noEmit typecheck)

…ndlers

ref https://linear.app/ghost/issue/PLA-96/spike-rolldown-vite-drop-in-build-benchmarks

react-world-flags is a CommonJS module shaped `{ __esModule: true, default: Component }`.
esbuild/Rollup unwrap the default automatically, but Rolldown returns the wrapper object,
so `<ReactFlag/>` renders an object and crashes the analytics location/source views. This
adds a bundler-agnostic interopDefault() unwrap so the component resolves under both
toolchains. Behavior-preserving under the current Rollup build; unblocks the Rolldown
evaluation without changing anything for Rollup.
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@nx-cloud

nx-cloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 0efd7fb

Command Status Duration Result
nx run @tryghost/admin-x-settings:test:acceptance ✅ Succeeded 10m 32s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded <1s View ↗
nx run-many -t test:unit -p @tryghost/shade,@tr... ✅ Succeeded 5m 51s View ↗
nx run @tryghost/admin:build ✅ Succeeded 4m 24s View ↗
nx run ghost-admin:test ✅ Succeeded 2m 37s View ↗
nx run-many -t lint -p @tryghost/shade,@tryghos... ✅ Succeeded 2m 6s View ↗
nx run @tryghost/activitypub:test:acceptance ✅ Succeeded 1m 4s View ↗
nx run ghost:build:assets ✅ Succeeded 2s View ↗
nx run ghost:build:tsc ✅ Succeeded 6s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-06 17:52:56 UTC

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The change modifies how the react-world-flags module is imported in the Flag component. Instead of a direct default import, the module is now imported as a namespace, and a new interopDefault helper function unwraps the default export to handle differences between CommonJS and ESM bundler outputs. The resulting ReactFlag reference is derived from this helper. No other logic or the exported component API is changed.

Related PRs: None identified.

Suggested labels: bug, dependencies

Suggested reviewers: None identified.

Poem

A rabbit hopped through bundler maze,
Unwrapping flags in tricky ways,
Default or namespace, CommonJS too,
interopDefault sees it through,
Now flags fly straight, no wrapper haze. 🐰🏳️

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: react-world-flags interop so Flag works under both bundlers.
Description check ✅ Passed The description accurately explains the Flag interop change and matches the files and objectives in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch steve/pla-96-a-react-world-flags-interop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
apps/shade/src/components/ui/flag.tsx (3)

3-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Verify test coverage exercises both export shapes.

This fix's whole purpose is handling two divergent module shapes (fully-unwrapped default vs. Rolldown's wrapper-preserving default). The referenced unit test suite runs under a single bundler, so it likely only exercises one branch of interopDefault. Consider mocking react-world-flags with both shapes (plain component, and {__esModule: true, default: {__esModule: true, default: Component}}) to lock in the fix and guard against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shade/src/components/ui/flag.tsx` around lines 3 - 18, The interop logic
in interopDefault and its ReactFlag usage should be covered by tests for both
module shapes, since the current suite likely only exercises one bundler
behavior. Update the unit tests for flag.tsx to mock react-world-flags as a
plain component export and as the nested wrapper shape ({ __esModule: true,
default: { __esModule: true, default: Component } }) and assert ReactFlag
resolves correctly in both cases.

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve FlagProps typing instead of Record<string, unknown>.

react-world-flags's types (@types/react-world-flags@1.6.0) declare a real FlagProps (code, fallback, plus passthrough HTMLProps<HTMLImageElement>). Typing ReactFlag as React.ComponentType<Record<string, unknown>> drops that contract, so a typo or wrong value type on the JSX usage below (Lines 42-48) would silently compile.

♻️ Proposed fix to retain prop typing
+type FlagComponentProps = React.ComponentProps<typeof ReactWorldFlags.default>;
-const ReactFlag = interopDefault<React.ComponentType<Record<string, unknown>>>(ReactWorldFlags);
+const ReactFlag = interopDefault<React.ComponentType<FlagComponentProps>>(ReactWorldFlags);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shade/src/components/ui/flag.tsx` at line 18, The ReactWorldFlags
wrapper is being typed too loosely in ReactFlag, which drops the real FlagProps
contract and allows invalid JSX props to compile silently. Update the
interopDefault typing in the flag component to preserve the library’s FlagProps
(including code, fallback, and HTMLProps<HTMLImageElement> passthrough) so the
Flag usage in the same component continues to get proper type checking.

9-17: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No fallback guard if unwrap fails to find a usable component.

If a future bundler/runtime returns a shape neither branch anticipates (e.g., a third wrapping layer), interopDefault silently returns that value via as T, reproducing the exact crash this PR fixes, just later and harder to trace. Consider a lightweight dev-only assertion so a bad shape fails loudly at the source instead of surfacing as a cryptic React render error.

🛡️ Proposed dev-time guard
     if (value && typeof value === 'object' && '__esModule' in value && 'default' in value) {
         value = (value as {default: unknown}).default;
     }
+    if (process.env.NODE_ENV !== 'production' && !value) {
+        // eslint-disable-next-line no-console
+        console.error('interopDefault: unable to resolve a component from module', mod);
+    }
     return value as T;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shade/src/components/ui/flag.tsx` around lines 9 - 17, The
interopDefault helper in flag.tsx can still return an unusable value when module
shapes are unexpectedly nested, so add a lightweight dev-only guard in
interopDefault that validates the final unwrapped value before casting. If the
resolved value is not a valid component-like export, fail loudly with a clear
assertion/error instead of returning it through as T, so bad bundler/runtime
shapes are caught at the source.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/shade/src/components/ui/flag.tsx`:
- Around line 3-18: The interop logic in interopDefault and its ReactFlag usage
should be covered by tests for both module shapes, since the current suite
likely only exercises one bundler behavior. Update the unit tests for flag.tsx
to mock react-world-flags as a plain component export and as the nested wrapper
shape ({ __esModule: true, default: { __esModule: true, default: Component } })
and assert ReactFlag resolves correctly in both cases.
- Line 18: The ReactWorldFlags wrapper is being typed too loosely in ReactFlag,
which drops the real FlagProps contract and allows invalid JSX props to compile
silently. Update the interopDefault typing in the flag component to preserve the
library’s FlagProps (including code, fallback, and HTMLProps<HTMLImageElement>
passthrough) so the Flag usage in the same component continues to get proper
type checking.
- Around line 9-17: The interopDefault helper in flag.tsx can still return an
unusable value when module shapes are unexpectedly nested, so add a lightweight
dev-only guard in interopDefault that validates the final unwrapped value before
casting. If the resolved value is not a valid component-like export, fail loudly
with a clear assertion/error instead of returning it through as T, so bad
bundler/runtime shapes are caught at the source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef0a0f07-62dc-40e2-88c9-05fbffd5b17a

📥 Commits

Reviewing files that changed from the base of the PR and between 498d2a0 and 0efd7fb.

📒 Files selected for processing (1)
  • apps/shade/src/components/ui/flag.tsx

9larsons added a commit that referenced this pull request Jul 6, 2026
ref https://linear.app/ghost/issue/PLA-96/spike-rolldown-vite-drop-in-build-benchmarks
country-flag-icons is true ESM, so the Flag component no longer needs the
CJS interop shim added in #29137 to work around react-world-flags's
{__esModule, default} shape under Rolldown; supersedes #29137.
@9larsons 9larsons marked this pull request as draft July 6, 2026 19:53
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