Skip to content

Conversation

@rtibbles
Copy link
Member

Summary

  • Fixes erroneous import from 'lodash' which causes the entire library to be bundled
  • Enables terser code mangling
  • Code splits vue-intl polyfill data
  • Changes corejs polyfilling strategy for kolibri-sandbox to prevent erroneous corejs imports in plugins that import it directly

Reviewer guidance

The build should still work as expected, and it will be worth testing this on Safari to ensure the polyfilling behaviour is still intact.

Manual testing of HTML5, H5P, and Bloompub resources would also be useful.

🤖 This was created by Claude Code. @rtibbles then reviewed the generated output, and did iterative rounds of updates before making it ready for review 🤖

@rtibbles
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Changes include webpack and babel configuration updates for build optimization, refactoring Vue Intl locale data loading to be dynamic and lazy-loaded based on locale parameter, removing explicit core-js polyfill imports in favor of configuration-driven injection, and updating i18n setup to load locale data asynchronously.

Changes

Cohort / File(s) Summary
Build Configuration
packages/kolibri-build/src/webpack.config.base.js
TerserPlugin mangle option changed from false to { safari10: true } to activate it, but maintain safari10 compatibilty.
Polyfill Cleanup
packages/kolibri-sandbox/src/iframe.js, packages/kolibri-sandbox/src/xAPI/xAPIConstants.js, packages/kolibri-sandbox/src/xAPI/xAPISchema.js, packages/kolibri-sandbox/babel.config.js, packages/kolibri-sandbox/webpack.config.js
Removed explicit core-js polyfill imports (array/includes, object/assign, object/entries, object/values, promise, string/starts-with, web/url, and set). No functional code changes; polyfill handling delegated to configuration. Babel preset-env switches from useBuiltIns: false to useBuiltIns: 'usage' with corejs: '3.46' for selective polyfill injection. Webpack Babel loader exclude pattern simplified to exclude all node_modules instead of specific module filtering.
Vue Intl Locale Data Refactoring
packages/kolibri-i18n/src/intl_code_gen.js, packages/kolibri/utils/internal/vue-intl-locale-data.js
Converted locale data loading from static aggregation to dynamic, lazy-loaded switch-based cases per locale. Added uniqBy deduplication of languageInfo entries. Function signature changed to accept locale parameter and return Promise-wrapped lazy-loaded module via require.ensure.
i18n Setup Refactoring
packages/kolibri/utils/i18n.js
Updated _setUpVueIntl to load locale data asynchronously, deriving language code from current language and awaiting importVueIntlLocaleData. Control flow now uses .then(resolve, reject) to defer i18n readiness until locale data is loaded.
Import Optimization
packages/kolibri/composables/useUser.js
Updated lodash import to use subpath lodash/pick instead of default import; no runtime or API changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Vue Intl locale data lazy-loading implementation: Verify that dynamic require.ensure patterns work correctly across locales and that Promise-based loading doesn't introduce race conditions or initialization order issues.
  • i18n async setup control flow: Ensure the deferred Promise resolution properly gates i18n readiness and that all dependent code awaits locale data loading.
  • Webpack Babel loader behavioral change: Confirm that the simplified exclude pattern (all node_modules) doesn't inadvertently process or skip necessary transformations for the build output.
  • Polyfill removal verification: Validate that removing explicit core-js imports doesn't break functionality in environments where the babel preset-env configuration doesn't automatically inject required polyfills.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main objective: reducing JS production build size by ~25%, which aligns with the changeset's focus on build optimization.
Description check ✅ Passed The description clearly relates to the changeset, explaining specific optimizations made: lodash import fix, terser mangling, vue-intl code splitting, and corejs strategy changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/analyze-webpack-build-01KdiNGodpTcF1ipRfguPxwF

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.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (4)
packages/kolibri-build/src/webpack.config.base.js (1)

110-117: Enabling Terser mangling: confirm no code relies on function/class names

Turning on mangle (with Safari 10 safeguards) is a solid win for bundle size, but it can break any code that inspects Function.name / constructor.name or otherwise depends on stable symbol names at runtime. Please verify there are no such dependencies (including in third‑party libs you bundle) and that the production build still works across your supported browsers. The duplicated safari10 setting (inside mangle and at the top level) is redundant but harmless if you prefer to keep it.

packages/kolibri-i18n/src/intl_code_gen.js (1)

5-6: Deduping vue‑intl locales by base code with uniqBy is appropriate

Using uniqBy(languageInfo, l => l.intl_code.split('-')[0]) keeps only one entry per base locale (e.g. en, es), which matches vue‑intl’s use of base codes and avoids generating duplicate case blocks. Note that generateVueIntlItems returns undefined for ach and any intentionally skipped locales; Array.prototype.join('') will effectively drop those entries, so behavior is correct. If you want to make this more explicit, you could optionally filter truthy values before join:

uniqBy(languageInfo, l => l.intl_code.split('-')[0])
  .map(generateVueIntlItems)
  .filter(Boolean)
  .join('')

Also applies to: 128-134

packages/kolibri/utils/i18n.js (2)

234-234: Consider using the existing languageIdToCode helper.

The inline split('-')[0] duplicates the logic from the languageIdToCode function (lines 133-135). Using the existing helper would follow DRY principles.

Apply this diff:

-  const vueIntlLanguageCode = currentLanguage.split('-')[0];
+  const vueIntlLanguageCode = languageIdToCode(currentLanguage);

235-236: Consider renaming requireLocaleData for clarity.

The variable requireLocaleData is the resolved function from the Promise, but the name reads like an action. Consider a name that better conveys it's a loader function, such as getLocaleData or localeDataLoader.

Example:

-  return importVueIntlLocaleData(vueIntlLanguageCode).then(requireLocaleData => {
-    const localeData = requireLocaleData();
+  return importVueIntlLocaleData(vueIntlLanguageCode).then(getLocaleData => {
+    const localeData = getLocaleData();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d26f50b and c04b9cd.

📒 Files selected for processing (10)
  • packages/kolibri-build/src/webpack.config.base.js (1 hunks)
  • packages/kolibri-i18n/src/intl_code_gen.js (3 hunks)
  • packages/kolibri-sandbox/babel.config.js (1 hunks)
  • packages/kolibri-sandbox/src/iframe.js (0 hunks)
  • packages/kolibri-sandbox/src/xAPI/xAPIConstants.js (0 hunks)
  • packages/kolibri-sandbox/src/xAPI/xAPISchema.js (0 hunks)
  • packages/kolibri-sandbox/webpack.config.js (1 hunks)
  • packages/kolibri/composables/useUser.js (1 hunks)
  • packages/kolibri/utils/i18n.js (3 hunks)
  • packages/kolibri/utils/internal/vue-intl-locale-data.js (1 hunks)
💤 Files with no reviewable changes (3)
  • packages/kolibri-sandbox/src/xAPI/xAPISchema.js
  • packages/kolibri-sandbox/src/xAPI/xAPIConstants.js
  • packages/kolibri-sandbox/src/iframe.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: All file linting
  • GitHub Check: Frontend tests
  • GitHub Check: Build WHL file / Build WHL
🔇 Additional comments (7)
packages/kolibri/composables/useUser.js (1)

11-11: Excellent fix for bundle size reduction!

This change from 'lodash' to 'lodash/pick' is the critical fix mentioned in the PR description. Importing from the main lodash entry point bundles the entire library (~70KB+ minified), while the subpath import only includes the pick function (~1-2KB), enabling proper tree-shaking.

packages/kolibri-sandbox/babel.config.js (1)

6-7: Polyfill strategy switch to useBuiltIns: 'usage' with core‑js 3.46

This is a sensible way to keep sandbox polyfills lean while relying on Babel to inject only what’s used. Please double‑check that core-js 3.46 is installed in this package’s dependency graph and that the sandbox still behaves correctly on older/Safari browsers now that you’re depending on preset‑env’s injected imports instead of explicit core‑js imports.

packages/kolibri-sandbox/webpack.config.js (1)

40-43: Simplified JS loader exclude is conventional; verify node_modules don’t need transpilation

Using exclude: /node_modules/ here is a straightforward way to ensure only sandbox sources go through Babel, which should help keep the config simple and the build lean. Just confirm that none of the JS you pull from node_modules ships untranspiled syntax that your target browsers (especially Safari) can’t handle, since those files will now bypass Babel.

packages/kolibri-i18n/src/intl_code_gen.js (1)

53-55: New async vue‑intl loader generation: ensure callers pass base locale and handle Promises

The generated module now exports function (locale) { switch (locale) { … } } where each case returns a Promise that resolves to a loader for the correct vue‑intl locale data (with polyfill fallback via ./polyfills/vue-intl/*.js when necessary). This looks consistent with the goal of per‑locale code‑splitting. Please verify that:

  • Callers now treat vue-intl-locale-data.js as an async API (await/then) rather than expecting a synchronously populated array, and
  • The locale passed at runtime is the base code (intl_code.split('-')[0], e.g. 'en', 'es'), matching the case '${vue_intl_code}' generated here; otherwise they’ll fall through to the default 'en' case.

Also applies to: 58-69, 101-110, 114-124

packages/kolibri/utils/internal/vue-intl-locale-data.js (1)

11-205: Async, per‑locale vue‑intl loader: confirm all call sites are updated

This generated module now exposes a Promise‑based, per‑locale loader using require.ensure for each vue‑intl locale (with a polyfill for ht and a default fallback to English). The structure matches the updated code generator and is well‑suited for code‑splitting. Please confirm that:

  • All consumers of packages/kolibri/utils/internal/vue-intl-locale-data.js now call it with a locale argument and handle the returned Promise, and
  • The locale value is normalized to the same base codes used here ('ar', 'bn', 'pt', etc.), so you consistently hit the intended case instead of the default.

Given the PR goal, it would also be good to explicitly exercise a few locales (including ones using the polyfill path like ht) in Safari to ensure that async loading and polyfilling behave as expected.

packages/kolibri/utils/i18n.js (2)

232-239: LGTM! Async locale data loading implemented correctly.

The asynchronous loading of vue-intl locale data successfully implements code-splitting as intended in the PR objectives. Key aspects:

  • The _i18nReady flag is correctly set only after locale data loads, preventing premature use of translation functions
  • Returning the Promise enables proper chaining in i18nSetup
  • Error handling will propagate correctly through the Promise chain

255-255: LGTM! Promise chaining ensures i18n is fully ready.

The updated calls to _setUpVueIntl().then(resolve, reject) correctly ensure that the i18nSetup Promise doesn't resolve until vue-intl locale data is fully loaded. This maintains the contract that i18n setup is complete before the Promise resolves.

Also applies to: 273-273

@github-actions
Copy link
Contributor

github-actions bot commented Nov 14, 2025

@rtibbles rtibbles marked this pull request as draft November 14, 2025 12:54
claude and others added 3 commits November 14, 2025 16:23
This commit implements three high-priority fixes identified in the
webpack bundle analysis that will reduce total bundle size by an
estimated 2.75-4.75 MB (13-23% reduction).

- File: packages/kolibri-build/src/webpack.config.base.js
- Change: mangle: false → mangle: { safari10: true }
- Expected savings: 2-4 MB across all bundles

Terser mangling was disabled, preventing variable name shortening
which typically provides 30-40% size reduction. This single-line
change enables it with Safari 10 compatibility.

- File: packages/kolibri/composables/useUser.js
- Change: import { pick } from 'lodash' → import pick from 'lodash/pick'
- Expected savings: ~450 KB in core bundle

A single bad import was causing webpack to bundle the entire lodash
library (459 KB) instead of just the pick function (~5 KB).
         packages/kolibri/utils/i18n.js (consumer)
         packages/kolibri/utils/internal/vue-intl-locale-data.js (generated)
- Expected savings: ~300 KB

Root cause: The intl polyfill locale data was correctly lazy-loaded,
but vue-intl locale data was not. The code generator was producing
synchronous require() for all 30+ vue-intl locales, bundling them
in the main chunk (336 KB total).

Fix: Updated code generators to use import() pattern and updated consumption code to handle async loading. Now only
the current language's vue-intl locale data is loaded.

Defer registration of nav components which can now happen before vue intl setup. This was always a problem for
consumers of the intl polyfill, but we had mostly not noticed it until now.
Updated kolibri-sandbox to use Babel's useBuiltIns: 'usage' instead of
explicit core-js imports. This eliminates ~330 KB of duplication across
bloompub_viewer, html5_viewer, and learn.app bundles.

Changes:
- Set babel useBuiltIns to 'usage' with corejs 3.46
- Removed explicit core-js imports from iframe.js and xAPI files
- Babel now auto-injects polyfills only during standalone build
- When imported into main bundles, no core-js duplication occurs

Expected savings: ~330 KB across 3 viewer bundles
@rtibbles rtibbles force-pushed the claude/analyze-webpack-build-01KdiNGodpTcF1ipRfguPxwF branch from e3b72a7 to 5870e8d Compare November 14, 2025 13:25
@rtibbles rtibbles marked this pull request as ready for review November 14, 2025 13:42
@marcellamaki marcellamaki self-assigned this Nov 18, 2025
Copy link
Member Author

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

One potentially suspect change in the kolibri-sandbox webpack configuration.

parallel: true,
terserOptions: {
mangle: false,
mangle: {
Copy link
Member Author

Choose a reason for hiding this comment

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

I can't precisely see why we've never enabled this option, but this is responsible for the majority of the savings here.

For context, this is where this was introduced (when we switched from one uglifier to Terser): 980aa2a

`;
const vueIntlHeader = `module.exports = function () {
const data = [];`;
const vueIntlHeader = `module.exports = function (locale) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I am not entirely sure why we never dynamically loaded these polyfills previously - I think because one of them was always needed, so we just bundled all of them.

Another alternative would be to add loading of these to the base HTML page, so that we don't end up with deferred loading, but I doubt it would make much difference timing wise.


return `
case '${language.intl_code}':
return new Promise(function(resolve) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This code was originally written before our webpack version supported import() statements, so just updating this as well while we're here.

};
/* eslint-enable vue/one-component-per-file */

module.exports = async function () {
Copy link
Member Author

Choose a reason for hiding this comment

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

Jest allows you to export an async function as the module.exports of the setup module, and it will wait for it to complete before starting any tests. This is needed as otherwise tests start before the i18n configuration has finished.

Previously this wasn't a problem because the vue i18n data was loaded synchronously, but with the changes above, this is now necessary.

* xAPI Constants
*/

import 'core-js/features/set';
Copy link
Member Author

Choose a reason for hiding this comment

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

We were individually polyfilling specific features in the kolibri-sandbox code. Much better to use babel present env to handle this for us instead.


export const registerNavItem = component => {
if (!navItems.includes(component)) {
if (!i18nReady.value) {
Copy link
Member Author

Choose a reason for hiding this comment

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

All the changes in this file were necessitated by the fact that the nav item registration process validates that the nav items have a label - which is invariably a translated string. All the changes here do is wait for i18nReady to be true, and then do the validation.

An alternative here would be to avoid the complexity and instead change the validation to avoid the string being evaluated early.

import store from 'kolibri/store';
import { LoginErrors, ERROR_CONSTANTS, UPDATE_MODAL_DISMISSED, UserKinds } from 'kolibri/constants';
import { pick } from 'lodash';
import pick from 'lodash/pick';
Copy link
Member Author

Choose a reason for hiding this comment

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

This is such a big gotcha, I wonder if we should make a linting rule for it - but because we're not using the ESM version of lodash, the previous code here ends up importing the entirety of lodash, not just the pick function.

module.exports = function (locale) {
switch (locale) {
case 'ar':
return new Promise(function (resolve) {
Copy link
Member Author

Choose a reason for hiding this comment

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

These changes are just a result of the change in automated code generation.

*
* Polyfill files are copied to ./polyfills/ directory to avoid external dependencies.
*/
module.exports = function () {
Copy link
Member Author

Choose a reason for hiding this comment

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

These changes are just a result of the change in automated code generation.

reject();
},
);
if (Object.prototype.hasOwnProperty.call(global, 'Intl') || skipPolyfill) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Now seemed as good a time as any to change this function to async/await to avoid it being so verbose.

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.

4 participants