Skip to content

Updated svg map library (CJS>ESM)#29139

Merged
9larsons merged 1 commit into
mainfrom
steve/pla-96-a-flag-esm-swap
Jul 7, 2026
Merged

Updated svg map library (CJS>ESM)#29139
9larsons merged 1 commit into
mainfrom
steve/pla-96-a-flag-esm-swap

Conversation

@9larsons

@9larsons 9larsons commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the CommonJS react-world-flags dependency with the true-ESM country-flag-icons in Shade's Flag component (apps/shade/src/components/ui/flag.tsx).

This supersedes / closes #29137, which added a CJS interopDefault shim to work around react-world-flags's {__esModule, default} CJS shape under Rolldown. country-flag-icons ships as native ESM ("type": "module", importindex.js), so no CJS interop shim is needed — the guiding principle here is "no CJS shims unless we have no choice", and this dependency gives us that choice.

What changed

  • pnpm-workspace.yaml: added country-flag-icons: 1.6.20 to the catalog (respects catalogMode: strict + minimumReleaseAge — 1.6.20 shipped 2026-07-01, well past the 3-day window).
  • apps/shade/package.json: added country-flag-icons: "catalog:"; removed react-world-flags and its @types/react-world-flags devDep (country-flag-icons ships its own types).
  • apps/shade/src/components/ui/flag.tsx: rewritten to render flags dynamically by ISO-3166 alpha-2 code.

Import approach

import {hasFlag} from 'country-flag-icons';
import * as Flags from 'country-flag-icons/react/3x2';

The countryCode is upper-cased (country-flag-icons keys are uppercase), guarded with hasFlag(code), and the component is looked up as Flags[code]. All native ESM imports — no interopDefault, no CJS shim.

Preserved API

The FlagProps API is unchanged: width, height, className, countryCode, fallback. The outer <div> wrapper, sizeStyle, rounded styling, and the default muted-placeholder fallback span are all preserved.

Invalid / missing code handling

If countryCode is empty, or hasFlag(code) is false (invalid/unknown code, e.g. XX), the component renders the fallback prop when provided, otherwise the default muted placeholder span — same behaviour the old fallback prop gave.

Bundle-size delta

  • Shade's own es/ output is unchanged (~1.6M total; flag.js 893 B → 1085 B). Shade externalizes all bare-specifier imports in vite.config.ts, so country-flag-icons is not bundled into Shade — it's a runtime dependency resolved by the consuming app.
  • Downstream (consuming app, e.g. admin): because the flag component is looked up dynamically (Flags[code]), Rollup cannot tree-shake, so the whole country-flag-icons/react/3x2 barrel is pulled in: ~324 KB raw / ~57 KB gzipped for all 265 flags.

This is meaningful but not egregious (not multiple MB). If we want to trim it later, the lighter alternative is per-code dynamic import() (e.g. import(\country-flag-icons/react/3x2/${code}`)`) so only the flags actually rendered are loaded — at the cost of an async/Suspense boundary in the component. Kept the straightforward synchronous version here.

Validation (stock Vite 7.3.2 + Rollup)

  • pnpm install — clean (+1 -8); lockfile has no react-world-flags refs.
  • pnpm --filter @tryghost/shade build — passes (type-check + vite build); built flag.js references country-flag-icons as external ESM imports, no react-world-flags.
  • pnpm --filter @tryghost/shade lint — passes.
  • pnpm --filter @tryghost/shade test:unit — 226 tests pass.

Draft

Draft so a human can eyeball actual flag rendering (valid codes, invalid XX, custom fallback) before finalizing.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

This PR replaces the react-world-flags dependency with country-flag-icons throughout the shade app. The apps/shade/package.json dependencies and devDependencies are updated to remove react-world-flags and @types/react-world-flags and add country-flag-icons. The pnpm-workspace.yaml catalog gains a pinned country-flag-icons version entry. The Flag component in apps/shade/src/components/ui/flag.tsx is reimplemented to use hasFlag and icon components from country-flag-icons, resolving icons via an uppercased country code and rendering a fallback when unavailable. The Storybook documentation description is updated to reference the new library.

Related Issues

Suggested labels: apps/shade, dependencies

Suggested reviewers: none identified from provided context

Poem
A rabbit hopped past flags of old,
Swapped worn banners for icons bold.
country-flag-icons now takes the stage,
Rendering codes on every page.
No more crashes, no more strife—
Just tiny flags, brought back to life. 🐰🏳️

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR fixes the crash, but it does not implement the linked issue's requested interopDefault shim or single-file scope. Either implement the requested interopDefault-based fix in apps/shade/src/components/ui/flag.tsx or update the linked issue to match the dependency swap approach.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The catalog, dependency, and storybook updates all support the Flag dependency swap; no unrelated changes are evident.
Title check ✅ Passed The title matches the main change: replacing the flag library and moving from CJS to ESM.
Description check ✅ Passed The description accurately describes the dependency swap, component rewrite, and preserved behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch steve/pla-96-a-flag-esm-swap

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.

@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 632ee13

Command Status Duration Result
nx run @tryghost/admin-x-settings:test:acceptance ✅ Succeeded 11m 2s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded <1s View ↗
nx run-many -t test:unit -p @tryghost/shade,@tr... ✅ Succeeded 7m 15s View ↗
nx run ghost:test:integration ✅ Succeeded 2m 48s View ↗
nx run @tryghost/admin:build ✅ Succeeded 4m 36s View ↗
nx run ghost:test:ci:integration ✅ Succeeded 4s View ↗
nx run ghost:test:legacy ✅ Succeeded 3m 1s View ↗
nx run ghost-admin:test ✅ Succeeded 2m 53s View ↗
Additional runs (7) ✅ Succeeded ... View ↗

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


☁️ Nx Cloud last updated this comment at 2026-07-06 19:15:32 UTC

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.26%. Comparing base (1f42ca3) to head (632ee13).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29139      +/-   ##
==========================================
- Coverage   74.29%   74.26%   -0.03%     
==========================================
  Files        1565     1565              
  Lines      135738   135738              
  Branches    16459    16447      -12     
==========================================
- Hits       100845   100806      -39     
- Misses      33864    33902      +38     
- Partials     1029     1030       +1     
Flag Coverage Δ
admin-tests 55.16% <ø> (-0.02%) ⬇️
e2e-tests 76.41% <ø> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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 force-pushed the steve/pla-96-a-flag-esm-swap branch from fd2eedf to 632ee13 Compare July 6, 2026 19:02
@9larsons 9larsons marked this pull request as ready for review July 6, 2026 19:48
@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.

@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.

Actionable comments posted: 1

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

33-45: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider passing an accessible name to the rendered flag SVG.

FlagIcon is rendered without a title, so the icon has no accessible name for screen readers. country-flag-icons React components support a title prop for this purpose.

♻️ Optional accessibility improvement
                     <FlagIcon
+                        title={code}
                         className='absolute w-auto max-w-none rounded-[2px]'
🤖 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 33 - 45, The FlagIcon
rendered in the flag component has no accessible name, so screen readers cannot
identify it. Update the FlagIcon usage in the flag component to pass a title
prop for the country name/label, using the existing FlagIcon render path and
keeping the defaultFallback unchanged. Use the FlagIcon symbol in this component
to locate the render site and ensure the accessible name is provided whenever
the SVG is rendered.
🤖 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.

Inline comments:
In `@apps/shade/src/components/ui/flag.tsx`:
- Line 31: The fallback handling in Flag is treating an explicit null the same
as an omitted value, so `fallback={null}` still renders the default placeholder.
Update the `Flag` component’s fallback selection logic to distinguish “not
provided” from “intentionally empty” by checking for `undefined` only, and
preserve `null` as a valid value that renders nothing. Use the existing
`FlagProps.fallback` and `defaultFallback` code path in `flag.tsx` to apply the
fix.

---

Nitpick comments:
In `@apps/shade/src/components/ui/flag.tsx`:
- Around line 33-45: The FlagIcon rendered in the flag component has no
accessible name, so screen readers cannot identify it. Update the FlagIcon usage
in the flag component to pass a title prop for the country name/label, using the
existing FlagIcon render path and keeping the defaultFallback unchanged. Use the
FlagIcon symbol in this component to locate the render site and ensure the
accessible name is provided whenever the SVG is rendered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 37ed421f-b800-4dc8-b5dd-1b53ec53ea67

📥 Commits

Reviewing files that changed from the base of the PR and between 498d2a0 and 632ee13.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/shade/package.json
  • apps/shade/src/components/ui/flag.stories.tsx
  • apps/shade/src/components/ui/flag.tsx
  • pnpm-workspace.yaml

const code = countryCode ? countryCode.toUpperCase() : '';
const FlagIcon = code && hasFlag(code) ? (Flags as Record<string, FlagComponent>)[code] : undefined;

const defaultFallback = fallback || <span className='h-[14px] w-[22px] rounded-[2px] bg-muted-foreground/20' style={sizeStyle}></span>;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

fallback of null doesn't render nothing — it still falls back to the default placeholder.

FlagProps.fallback is typed as React.ReactNode | null | undefined, implying null is a distinct, intentional value (e.g. "render nothing"). But fallback || <span .../> treats null the same as undefined since both are falsy, so an explicit fallback={null} still renders the default muted placeholder instead of nothing.

🐛 Proposed fix distinguishing "not provided" from "explicitly empty"
-    const defaultFallback = fallback || <span className='h-[14px] w-[22px] rounded-[2px] bg-muted-foreground/20' style={sizeStyle}></span>;
+    const defaultFallback = fallback === undefined
+        ? <span className='h-[14px] w-[22px] rounded-[2px] bg-muted-foreground/20' style={sizeStyle}></span>
+        : fallback;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultFallback = fallback || <span className='h-[14px] w-[22px] rounded-[2px] bg-muted-foreground/20' style={sizeStyle}></span>;
const defaultFallback = fallback === undefined
? <span className='h-[14px] w-[22px] rounded-[2px] bg-muted-foreground/20' style={sizeStyle}></span>
: fallback;
🤖 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 31, The fallback handling in
Flag is treating an explicit null the same as an omitted value, so
`fallback={null}` still renders the default placeholder. Update the `Flag`
component’s fallback selection logic to distinguish “not provided” from
“intentionally empty” by checking for `undefined` only, and preserve `null` as a
valid value that renders nothing. Use the existing `FlagProps.fallback` and
`defaultFallback` code path in `flag.tsx` to apply the fix.

@9larsons 9larsons added the ok to merge for me You can merge this on my behalf if you want. label Jul 6, 2026
@9larsons 9larsons changed the title Replaced CJS react-world-flags with ESM country-flag-icons in Flag Updated svg map library (CJS>ESM) Jul 7, 2026
@9larsons 9larsons merged commit 7d9f285 into main Jul 7, 2026
52 checks passed
@9larsons 9larsons deleted the steve/pla-96-a-flag-esm-swap branch July 7, 2026 13:56
9larsons added a commit that referenced this pull request Jul 7, 2026
ref https://linear.app/ghost/issue/PLA-96

- adds a `rolldown` named catalog (vite: npm:rolldown-vite@7.3.1) and points the 8
  admin-surface apps (posts, stats, admin, admin-x-framework, admin-x-settings,
  admin-x-design-system, shade, activitypub) at catalog:rolldown
- public UMD apps stay on stock vite until their own Rolldown config lands (#29141)
- the known Rolldown interop breakers are already fixed on main: shade Flag ESM
  swap (#29139), @svg-maps/world shim (#29143), @tryghost/i18n dual-format (#29144)
- verified: all 8 apps + the admin shell build clean under Rolldown; react/react-dom
  stay external (no dual-instance regression); only benign bundler-agnostic warnings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok to merge for me You can merge this on my behalf if you want.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant