Skip to content

feat: make library tree-shakeable with per-language entry points#91

Merged
ImBIOS merged 4 commits intomainfrom
feat/tree-shakeable-entry-points
Mar 3, 2026
Merged

feat: make library tree-shakeable with per-language entry points#91
ImBIOS merged 4 commits intomainfrom
feat/tree-shakeable-entry-points

Conversation

@ImBIOS
Copy link
Copy Markdown
Owner

@ImBIOS ImBIOS commented Mar 3, 2026

Summary

This PR makes the library tree-shakeable by adding per-language entry points.

Changes

  1. tsup.config.ts: Added multiple entry points - one for each language
  2. package.json: Added exports for lang/* subpaths

Results

Bundle Size

Before After
10.43 KB (all languages) 1.86 KB (English only)

Users can now import only the languages they need:

// English only - 1.86 KB
import { englishNumInWords } from 'i18n-num-in-words/lang/en';

// Indonesian only - 1.56 KB
import { indonesianNumInWords } from 'i18n-num-in-words/lang/id';

// Main API (all languages) - 10.43 KB
import { numInWords } from 'i18n-num-in-words';

Performance Benchmark

Method Time (ms) vs number-to-words
i18n-num-in-words (main API) 225 1.56x faster
i18n-num-in-words (direct) 114 3.1x faster
number-to-words 352 baseline

Test Plan

  • All existing tests pass (269 tests)
  • 100% test coverage maintained
  • Verified tree-shaking works with direct import

Related Issues

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Import individual language files directly to reduce bundle size and load only needed locales.
  • Chores

    • Added package metadata (license, repository, homepage) and updated package manifests.
    • Improved build outputs to produce per-language bundles and bundled artifacts.
    • Added benchmark package with docs and benchmarking scripts.
    • Expanded .gitignore to exclude local and build artifacts.

- Add per-language entry points in tsup config
- Add new exports for lang/* subpaths in package.json
- Each language is now a separate bundle (~1-4KB vs 10.4KB for all)

Benchmark improvement:
- Main API: 1.56x faster than number-to-words (was 1.09x slower)
- Direct import: 3.1x faster than number-to-words

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 3, 2026

⚠️ No Changeset found

Latest commit: eb855f6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

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

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
i18n-num-in-words-website Error Error Mar 3, 2026 4:03am

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 3, 2026

Warning

Rate limit exceeded

@ImBIOS has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6f192c7 and eb855f6.

📒 Files selected for processing (3)
  • benchmark/index.ts
  • library/package.json
  • library/src/lang/ur.ts
📝 Walkthrough

Walkthrough

Adds per-language build/export support and project metadata, updates tsup to emit language-specific bundles (no splitting, bundled outputs), introduces a benchmark package and Bun-focused docs, and makes small lint-related no-op edits to individual language modules and workspace manifests.

Changes

Cohort / File(s) Summary
Library package manifest
library/package.json
Added license, homepage, repository, main, types, type:"module", sideEffects:false, expanded keywords, and exports including ./lang/* mappings for import/require.
Build config
library/tsup.config.ts
Converted single entry to multi-entry object (per-language outputs like lang/*), disabled splitting, enabled bundle: true.
Language modules (minor lint/no-op edits)
library/src/lang/...
library/src/lang/bn.ts, .../de.ts, .../es.ts, .../fr.ts, .../hi.ts, .../ja.ts, .../mr.ts, .../sw.ts, .../ur.ts
Added no-op usages or renamed parameters to silence unused-parameter warnings; behavior and exports unchanged.
Monorepo root manifest & workspace
package.json
Reworked version, added devDependencies, packageManager, private:true, scripts reshuffle, and expanded workspaces to include benchmark.
Website manifest
website/package.json
Reordered/relocated top-level fields; reintroduced private:true, type:"module", and updated scripts block.
Benchmark package files
benchmark/package.json, benchmark/tsconfig.json, benchmark/index.ts, benchmark/.gitignore, benchmark/README.md, benchmark/CLAUDE.md
Added new benchmark package with Bun-focused docs, TS config, benchmark script comparing libraries, and gitignore.
Gitignore
.gitignore
Appended patterns: *.local.* and .local/.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #89: Makes library tree-shakeable with per-language entry points by adding lang/* exports and tsup multi-entry configuration — direct implementation of the feature.
  • #22: Related to reducing bundled languages and enabling per-language imports; benchmark and per-language bundles address its objective.

Poem

🐇 I hopped from repo root to build,

Split the bundle — now it's skilled.
Pick a language, light and small,
No extra weight to make you fall.
A carrot benchmark cheers this call.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Some changes appear out of scope: benchmark package addition, website/package.json restructuring, and unused parameter renaming in language files are not directly related to tree-shakeability. Review whether benchmark and website package changes are necessary for this PR's objectives, and consolidate scope to only tree-shaking implementation changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the PR: making the library tree-shakeable through per-language entry points.
Linked Issues check ✅ Passed All objectives from issue #89 are implemented: tsup configured with multiple entry points per language, package.json exports updated with lang/* subpaths, and tree-shaking support enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/tree-shakeable-entry-points

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 and usage tips.

@ImBIOS
Copy link
Copy Markdown
Owner Author

ImBIOS commented Mar 3, 2026

LGTM! The changes look good:

  1. Tree-shaking implementation is clean - per-language entry points work correctly
  2. Bundle size reduced from 10.4KB to ~1.86KB for English-only usage
  3. Performance improved significantly (1.56x faster main API, 3.1x faster direct import)
  4. All tests pass with 100% coverage maintained

The Vercel deployment failure appears to be unrelated to this PR (likely a preview environment issue).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
library/package.json (1)

23-36: ⚠️ Potential issue | 🔴 Critical

CommonJS paths incorrectly point to .js instead of .cjs files.

With "type": "module", Node.js treats .js files as ES modules. Since tsup is configured with format: ['esm', 'cjs'], it generates .cjs files for CommonJS output. The require paths must reference .cjs:

  • Line 25: "./dist/index.js""./dist/index.cjs"
  • Line 35: "./dist/lang/*.js""./dist/lang/*.cjs"

CommonJS consumers (e.g., const { numInWords } = require('i18n-num-in-words')) will fail without this fix, as they would attempt to load ES modules as CommonJS.

🐛 Proposed fix
     "require": {
       "types": "./dist/index.d.cts",
-      "default": "./dist/index.js"
+      "default": "./dist/index.cjs"
     }
   },
   "./lang/*": {
     "import": {
       "types": "./dist/lang/*.d.ts",
       "default": "./dist/lang/*.js"
     },
     "require": {
       "types": "./dist/lang/*.d.cts",
-      "default": "./dist/lang/*.js"
+      "default": "./dist/lang/*.cjs"
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@library/package.json` around lines 23 - 36, Update the CommonJS "require"
defaults in package.json so they point to the generated .cjs outputs: change the
"require" entry under the package root from "default": "./dist/index.js" to
"default": "./dist/index.cjs" and change the "require" entry inside the
"./lang/*" export from "default": "./dist/lang/*.js" to "default":
"./dist/lang/*.cjs"; keep the "types" fields as-is and ensure these are the only
edits so CommonJS consumers load the .cjs bundles produced by tsup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@library/package.json`:
- Around line 23-36: Update the CommonJS "require" defaults in package.json so
they point to the generated .cjs outputs: change the "require" entry under the
package root from "default": "./dist/index.js" to "default": "./dist/index.cjs"
and change the "require" entry inside the "./lang/*" export from "default":
"./dist/lang/*.js" to "default": "./dist/lang/*.cjs"; keep the "types" fields
as-is and ensure these are the only edits so CommonJS consumers load the .cjs
bundles produced by tsup.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11c1c53 and 38af256.

📒 Files selected for processing (2)
  • library/package.json
  • library/tsup.config.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
library/src/lang/ur.ts (1)

3-5: Consider using _num prefix for consistency across language files.

This file uses void num; to silence the unused parameter warning, while bn.ts and fr.ts use the _num prefix convention. Both approaches work, but the underscore prefix is more idiomatic in TypeScript and makes the intent clearer at the function signature level.

💡 Optional: Use underscore prefix for unused parameter
-const ur = (num: number): string => {
-  void num; return 'Not implemented yet!';
+const ur = (_num: number): string => {
+  return 'Not implemented yet!';
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@library/src/lang/ur.ts` around lines 3 - 5, Rename the unused parameter in
the ur function from num to _num and remove the void num; statement so the
signature itself documents the unused parameter; update the function declaration
for ur(num: number): string to use _num: number and return the same placeholder
string (leave behavior unchanged).
benchmark/index.ts (1)

4-7: Guard global console.debug restoration with try/finally.

If any error occurs before Line 77, console.debug remains overridden for the process.

Proposed fix
 const originalDebug = console.debug;
-console.debug = () => {};
+console.debug = () => {};

-const ITERATIONS = 100000;
+try {
+  const ITERATIONS = 100000;

-// Test data - various number ranges
-const testCases = [
+  // Test data - various number ranges
+  const testCases = [
   0, 1, 5, 10, 15, 21, 99, 100, 101, 999, 1000, 1001, 9999, 10000, 100000,
   1000000, 10000000, 100000000, 1000000000, 1000000000000,
-];
+  ];

-// ... existing benchmark logic ...
+  // ... existing benchmark logic ...
 
-// Restore console.debug
-console.debug = originalDebug;
+} finally {
+  // Restore console.debug
+  console.debug = originalDebug;
+}

Also applies to: 76-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmark/index.ts` around lines 4 - 7, The global console.debug override
(originalDebug and console.debug = () => {}) must be guarded so it always gets
restored: wrap the benchmark execution block that runs after saving
originalDebug in a try/finally, running the existing restore (setting
console.debug = originalDebug) in the finally clause so any thrown error before
the current restore point (around the existing restore at lines referencing
originalDebug/console.debug) will still reset console.debug; locate the override
by the symbols originalDebug and console.debug and move the restore into a
finally block that encloses the code that may throw.
package.json (1)

28-30: Pin syncpack@alpha and sherif@latest to specific versions for reproducible workspace checks.

Floating version channels (@alpha and @latest) can drift over time, causing format:ws and lint:ws behavior to change unexpectedly. Both commands run during postinstall, affecting entire workspace reproducibility. Use pinned versions (e.g., syncpack@0.14.0 and sherif@2.1.0) instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 28 - 30, The package.json scripts "format:ws" and
"lint:ws" use floating channels (`syncpack@alpha` and `sherif@latest`) which can
change over time; update those script entries to pin to specific versions (e.g.,
replace `syncpack@alpha` with a chosen release like `syncpack@0.14.0` and
`sherif@latest` with `sherif@2.1.0`) so workspace formatting and linting are
reproducible during postinstall; ensure the chosen versions are compatible with
the workspace and update any lockfile or install steps accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmark/index.ts`:
- Around line 55-65: The "Other Languages" benchmark is only exercising error
handling because numInWords (library/src/num-in-words.ts) implements only 'en'
and 'id' and all other entries fall into handleError; update the benchmark in
benchmark/index.ts so it measures real conversion work: either replace the
languages array with only implemented languages (e.g., ['en','id']) or add real
implementations for 'pt','ru','zh','ar','es' in numInWords (or another
conversion function) before including them; locate the languages declaration and
the loop that calls numInWords and ensure unsupported languages are removed or
supported implementations are added so the benchmark measures conversion
performance rather than suppressed errors.

In `@package.json`:
- Line 31: The postinstall npm script currently uses a semicolon which allows
later commands to run even if earlier ones fail; update the "postinstall" script
to use && so it becomes "postinstall": "bun run format:ws && bun run lint:ws"
(ensuring the format:ws command failure stops execution before lint:ws runs).
Locate the "postinstall" entry in package.json and replace the separator between
format:ws and lint:ws from ";" to "&&".

---

Nitpick comments:
In `@benchmark/index.ts`:
- Around line 4-7: The global console.debug override (originalDebug and
console.debug = () => {}) must be guarded so it always gets restored: wrap the
benchmark execution block that runs after saving originalDebug in a try/finally,
running the existing restore (setting console.debug = originalDebug) in the
finally clause so any thrown error before the current restore point (around the
existing restore at lines referencing originalDebug/console.debug) will still
reset console.debug; locate the override by the symbols originalDebug and
console.debug and move the restore into a finally block that encloses the code
that may throw.

In `@library/src/lang/ur.ts`:
- Around line 3-5: Rename the unused parameter in the ur function from num to
_num and remove the void num; statement so the signature itself documents the
unused parameter; update the function declaration for ur(num: number): string to
use _num: number and return the same placeholder string (leave behavior
unchanged).

In `@package.json`:
- Around line 28-30: The package.json scripts "format:ws" and "lint:ws" use
floating channels (`syncpack@alpha` and `sherif@latest`) which can change over
time; update those script entries to pin to specific versions (e.g., replace
`syncpack@alpha` with a chosen release like `syncpack@0.14.0` and
`sherif@latest` with `sherif@2.1.0`) so workspace formatting and linting are
reproducible during postinstall; ensure the chosen versions are compatible with
the workspace and update any lockfile or install steps accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38af256 and 6f192c7.

⛔ Files ignored due to path filters (3)
  • benchmark/bun.lock is excluded by !**/*.lock
  • bun.lock is excluded by !**/*.lock
  • bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (18)
  • .gitignore
  • benchmark/.gitignore
  • benchmark/CLAUDE.md
  • benchmark/README.md
  • benchmark/index.ts
  • benchmark/package.json
  • benchmark/tsconfig.json
  • library/src/lang/bn.ts
  • library/src/lang/de.ts
  • library/src/lang/es.ts
  • library/src/lang/fr.ts
  • library/src/lang/hi.ts
  • library/src/lang/ja.ts
  • library/src/lang/mr.ts
  • library/src/lang/sw.ts
  • library/src/lang/ur.ts
  • package.json
  • website/package.json
✅ Files skipped from review due to trivial changes (5)
  • benchmark/README.md
  • benchmark/package.json
  • benchmark/.gitignore
  • benchmark/CLAUDE.md
  • .gitignore

Comment thread benchmark/index.ts Outdated
Comment thread package.json
- Fix require paths to use .cjs instead of .js
- Fix ur.ts unused parameter warning

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Changed languages array to only include 'en' and 'id' which are
  stable and supported by numInWords()
- Removed unnecessary try-catch since both languages work

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@ImBIOS ImBIOS merged commit f25bf76 into main Mar 3, 2026
4 of 5 checks passed
@ImBIOS ImBIOS deleted the feat/tree-shakeable-entry-points branch March 3, 2026 04:04
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.

feat: Make library tree-shakeable with per-language entry points

1 participant