Skip to content

fix(i18n): use .js extensions on dayjs subpath imports for valid Node ESM#3231

Merged
oliverlaz merged 3 commits into
masterfrom
fix/dayjs-esm-subpath-extensions
Jul 2, 2026
Merged

fix(i18n): use .js extensions on dayjs subpath imports for valid Node ESM#3231
oliverlaz merged 3 commits into
masterfrom
fix/dayjs-esm-subpath-extensions

Conversation

@oliverlaz

@oliverlaz oliverlaz commented Jul 2, 2026

Copy link
Copy Markdown
Member

🎯 Goal

The published ESM bundle imported dayjs plugins and locales via extensionless subpaths (e.g. import 'dayjs/plugin/calendar', import 'dayjs/locale/de'). dayjs ships no exports map, so Node's native ESM resolver requires the file extension and rejects these specifiers with ERR_MODULE_NOT_FOUND.

Bundlers (Turbopack, Webpack, Vite) resolve extensionless subpaths fine, so the bug is invisible in most setups. But Node's native loader does not β€” and Next.js 16 loads stream-chat-react as a server external during "Collecting page data", so the consumer build fails:

Failed to load external module stream-chat-react...: ERR_MODULE_NOT_FOUND
Cannot find module '.../dayjs/plugin/calendar' imported from .../useNotificationApi.<hash>.mjs
Did you mean to import "dayjs/plugin/calendar.js"?

Regressed in #3188 β€” first shipped in 14.4.0. Verified by loading each published dist/es/index.mjs under Node's native ESM loader: 14.3.0 loads cleanly, 14.4.0 (and every release since) throws ERR_MODULE_NOT_FOUND β†’ dayjs/plugin/calendar. #3188 broadened the Vite external subpath regex from (\/[\w-]+)? (one segment) to (\/.+)? (any depth). dayjs/plugin/calendar is a two-segment subpath, so the old regex didn't match it and Vite bundled the plugin inline (no bare import in the output); the new regex externalizes it, emitting the extensionless specifier verbatim. The extensionless imports had always been in source β€” the build used to bundle them away.

Note that broadening the regex was itself a deliberate ESM-correctness fix (keeping CJS require() glue out of the ESM output), so externalizing subpaths is intended. This PR keeps the externalization and makes the specifiers valid, rather than reverting it and reintroducing the CJS-glue problem.

πŸ›  Implementation details

The fix β€” add .js to every dayjs subpath import in shipped source, so the specifiers Vite externalizes (emits verbatim) are valid ESM:

  • src/i18n/Streami18n.ts β€” 8 plugin imports + 12 locale side-effect imports
  • src/context/TranslationContext.tsx β€” 2 plugin imports
  • src/i18n/utils.ts β€” 1 type-only import (keeps the emitted .d.ts consistent)
  • also corrected a stale JSDoc example and the registerTranslation log message

.js is safe across every consumer: Node ESM (the fix), bundlers, and TS types (moduleResolution: "bundler" maps .js β†’ .d.ts). The CJS output is unaffected.

Regression guard β€” the existing validate-cjs smoke test loads the bundle with require(), whose CJS resolver tolerates extensionless imports, so it never caught this. Added the missing ESM counterpart:

  • scripts/validate-esm-node-bundle.mjs β€” imports dist/es/index.mjs under Node's native loader
  • yarn validate-esm script + a CI step in ci.yml right after validate-cjs

Verified: validate-esm passes on a healthy build and fails with ERR_MODULE_NOT_FOUND when a .js is stripped from a dayjs import in the built output. yarn build, yarn types, ESLint, and the i18n tests all pass.

Also included β€” toolchain bump (independent of the fix, bundled here): vite 8.0.14 β†’ 8.1.3, vitest + @vitest/coverage-v8 4.1.7 β†’ 4.1.9, @vitest/eslint-plugin 1.6.18 β†’ 1.6.20 (pulls rolldown 1.0.2 β†’ 1.1.3); the examples/* workspaces bumped to match; .yarnrc.yml lowers npmMinimalAgeGate to 1d and preapproves vite, and drops enableHardenedMode. Diffing dist/es + dist/cjs before/after the bump shows no behavioral or API-surface change β€” only additional /* @__PURE__ */ annotations and one internal import alias, both from the rolldown bump.

🎨 UI Changes

None β€” packaging/build fix, no runtime or visual behavior change.

Summary by CodeRabbit

  • Bug Fixes

    • Improved ESM bundle compatibility by updating import paths to use explicit .js extensions.
    • Added validation to catch Node ESM import issues earlier during CI.
    • Updated user-facing guidance for locale imports to match the new ESM format.
  • Chores

    • Updated several build and test tool versions.
    • Adjusted package manager settings and example project dependencies for the latest releases.

oliverlaz added 2 commits July 2, 2026 15:20
… ESM

dayjs ships no "exports" map, so Node's native ESM resolver requires a file
extension on subpath imports. Because vite externalizes dayjs, the built
dist/es/*.mjs preserved the extensionless specifiers verbatim, producing
invalid ESM: bundlers (Turbopack/Webpack/Vite) resolve them fine, but Node's
native loader throws ERR_MODULE_NOT_FOUND -- e.g. Next.js 16 loading the SDK as
a server external during "Collecting page data".

Add .js to all dayjs plugin and locale subpath imports (8 plugins + 12 locales)
so the emitted ESM is valid under Node's native loader. Also fixes a stale
JSDoc example and the registerTranslation log message.
The existing validate-cjs smoke test loads the bundle with require(), whose CJS
resolver tolerates extensionless subpath imports -- so it never caught the
invalid ESM output that broke Node's native loader. Add an ESM counterpart that
imports dist/es/index.mjs under the native loader and wire it into CI right
after validate-cjs.

Verified: the check passes on a healthy build and fails with ERR_MODULE_NOT_FOUND
when a .js extension is stripped from a dayjs subpath import in the built output.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

πŸ“ Walkthrough

Walkthrough

Dayjs import paths are updated to use explicit .js extensions. A new Node script validates the built ESM bundle, and CI plus package and toolchain settings are updated to run and support that validation.

Changes

ESM compatibility fixes

Layer / File(s) Summary
Update Dayjs imports to .js extensions
src/context/TranslationContext.tsx, src/i18n/Streami18n.ts, src/i18n/types.ts, src/i18n/utils.ts
Dayjs plugin and locale imports are changed to .js-suffixed specifiers, including JSDoc examples and the missing-locale guidance string.
Add ESM validation script and CI/package.json wiring
scripts/validate-esm-node-bundle.mjs, package.json, .github/workflows/ci.yml
A new script imports the built ESM entrypoint to surface subpath import failures, and it is added as a validate-esm npm script and run in CI.
Update Yarn and Vite versions
.yarnrc.yml, package.json, examples/tutorial/package.json, examples/vite/package.json
Yarn config changes adjust hardened mode, npm age gating, and preapproved packages, while root and example package manifests bump Vite/Vitest-related dependency versions.

Estimated code review effort: 2 (Simple) | ~10 minutes

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Title check βœ… Passed The title clearly summarizes the main fix: adding .js extensions to Dayjs subpath imports for Node ESM compatibility.
Description check βœ… Passed The description matches the template and includes a clear goal, implementation details, and UI changes section.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dayjs-esm-subpath-extensions

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.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Size Change: +48 B (+0.01%)

Total Size: 798 kB

πŸ“¦ View Changed
Filename Size Change
dist/cjs/channel-detail.js 22.9 kB +2 B (+0.01%)
dist/cjs/index.js 259 kB +16 B (+0.01%)
dist/cjs/ReactPlayerWrapper.js 546 B +2 B (+0.37%)
dist/cjs/useChannelHeaderOnlineStatus.js 38 kB +3 B (+0.01%)
dist/cjs/useMessageComposerController.js 1.01 kB +2 B (+0.2%)
dist/cjs/useNotificationApi.js 52.8 kB +13 B (+0.02%)
dist/es/channel-detail.mjs 22.6 kB -1 B (0%)
dist/es/emojis.mjs 2.48 kB -1 B (-0.04%)
dist/es/index.mjs 256 kB +4 B (0%)
dist/es/useNotificationApi.mjs 51.5 kB +8 B (+0.02%)
ℹ️ View Unchanged
Filename Size
dist/cjs/audioProcessing.js 1.74 kB
dist/cjs/emojis.js 2.56 kB
dist/cjs/mp3-encoder.js 814 B
dist/css/channel-detail.css 2.84 kB
dist/css/emoji-picker.css 178 B
dist/css/emoji-replacement.css 456 B
dist/css/index.css 41.3 kB
dist/es/audioProcessing.mjs 1.65 kB
dist/es/mp3-encoder.mjs 768 B
dist/es/ReactPlayerWrapper.mjs 485 B
dist/es/useChannelHeaderOnlineStatus.mjs 37.4 kB
dist/es/useMessageComposerController.mjs 936 B

compressed-size-action

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 84.25%. Comparing base (f790520) to head (23424a6).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3231      +/-   ##
==========================================
+ Coverage   84.21%   84.25%   +0.04%     
==========================================
  Files         485      485              
  Lines       14872    14873       +1     
  Branches     4708     4709       +1     
==========================================
+ Hits        12524    12531       +7     
+ Misses       2348     2342       -6     

β˜” 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.

- root: vite 8.0.14 -> 8.1.3, vitest + @vitest/coverage-v8 4.1.7 -> 4.1.9,
  @vitest/eslint-plugin 1.6.18 -> 1.6.20 (pulls rolldown 1.0.2 -> 1.1.3)
- examples/tutorial, examples/vite: vite 8.1.3 + @vitejs/plugin-react 6.0.3
- .yarnrc.yml: lower npmMinimalAgeGate to 1d and preapprove vite so the fresh
  release installs; drop enableHardenedMode

Build output is unchanged apart from additional `/* @__PURE__ */` annotations
and one internal import alias, both from the rolldown bump -- no behavioral or
API-surface change (verified by diffing dist/es and dist/cjs across versions).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.yarnrc.yml (1)

15-17: πŸ”’ Security & Privacy | πŸ”΅ Trivial | πŸ’€ Low value

Consider a one-time bypass instead of permanently preapproving vite.

Adding vite to npmPreapprovedPackages permanently exempts all future vite releases from the 1-day age-gate quarantine, not just the version needed for this bump. Yarn supports a --no-time-gate flag on yarn add/yarn up for one-off bypasses, which would avoid permanently weakening the quarantine window for a package that will be installed frequently going forward.

πŸ€– 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 @.yarnrc.yml around lines 15 - 17, The change in npmPreapprovedPackages
permanently whitelists vite and bypasses the age gate for all future releases,
so revert that persistent exemption and use a one-time --no-time-gate bypass
when running yarn add or yarn up for the specific vite bump. Keep the existing
stream-chat entry if still needed, and avoid leaving vite in .yarnrc.yml so the
quarantine window remains intact going forward.
πŸ€– 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 @.yarnrc.yml:
- Around line 15-17: The change in npmPreapprovedPackages permanently whitelists
vite and bypasses the age gate for all future releases, so revert that
persistent exemption and use a one-time --no-time-gate bypass when running yarn
add or yarn up for the specific vite bump. Keep the existing stream-chat entry
if still needed, and avoid leaving vite in .yarnrc.yml so the quarantine window
remains intact going forward.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 342d4d2d-7556-4e31-bb85-39143f123fd9

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 31c9247 and 23424a6.

β›” Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
πŸ“’ Files selected for processing (4)
  • .yarnrc.yml
  • examples/tutorial/package.json
  • examples/vite/package.json
  • package.json
βœ… Files skipped from review due to trivial changes (1)
  • examples/vite/package.json

@oliverlaz oliverlaz merged commit 7663b18 into master Jul 2, 2026
14 checks passed
@oliverlaz oliverlaz deleted the fix/dayjs-esm-subpath-extensions branch July 2, 2026 14:01
github-actions Bot pushed a commit that referenced this pull request Jul 3, 2026
## [14.6.1](v14.6.0...v14.6.1) (2026-07-03)

### Bug Fixes

* bug bashing ChannelList + Channel ([#2474](#2474), [#2441](#2441), [#2393](#2393)) ([#3227](#3227)) ([f790520](f790520)), closes [GetStream/stream-chat-js#1788](GetStream/stream-chat-js#1788) [#2599](#2599) [#2599](#2599)
* **i18n:** use .js extensions on dayjs subpath imports for valid Node ESM ([#3231](#3231)) ([7663b18](7663b18)), closes [#3188](#3188) [#3188](#3188)
* **MessageList:** don't count thread replies toward channel unread UI state ([#3229](#3229)) ([ca5ed16](ca5ed16))
* **renderText:** recognize uppercase URL schemes ([#3226](#3226)) ([21d57e3](21d57e3))
* **renderText:** recognize uppercase URL schemes in message links ([6178513](6178513))
* return background colors to LinkPreviewCard and TypingIndicator ([a768e30](a768e30))
@stream-ci-bot

Copy link
Copy Markdown

πŸŽ‰ This PR is included in version 14.6.1 πŸŽ‰

The release is available on:

Your semantic-release bot πŸ“¦πŸš€

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants