font-awesomeviterollupcode-splittingdynamic-import

Vite / Rollup Incorrect Chunking


I'm having an issue importing Fontawesome icons into a Vue 3 Vite Project. Specifically the build files output from rollup are not chunked correctly. I've tried the two methods of importing icons from Fontawesome's documentation, however neither result in an optimal chunk output.

Any help with either finding another solution or finding anything stupid I've done to cause this would be much appreciated.

Direct Import

Import Documention

FaCat Icon

FaDog Icon

Importing the commonjs icon directly via import {faCat} from @fortawesome/pro-light-svg-icons/faCat which results in some specific cases to the icon data not being placed in the chunk generated by rollup. This results in the icon not always loading in until a specific unrelated chunk was loaded in the example below if chunkA.js was loaded first the icon would only show when chunkB.js had be loaded as well even though the shared data should be in faCat.js chunk.

This feels like a bug with rollups bundler algo to me, as it's creating a chunk for the icon correctly and importing it correctly. However seems to be putting the contents of the icon into another chunk. Happy to report this once I know it's not anything I've done.

I've not been able to replicate this issue with anything else (node_module or local) but I can't see anything specific to fontawesome that would cause it.

Example build file output

/*
 * index.js
 */
console.log('--- ENTRY START ---');
const h = u(
  () => import('./testA.a8e39a8f.js'),
  ['assets/testA.a8e39a8f.js', 'assets/faCat.6085f215.js']
),
  g = u(
    () => import('./testB.b39c5fd9.js'),
    ['assets/testB.b39c5fd9.js', 'assets/faCat.6085f215.js']
  );
(async () => {
  const { name: s, icon: r } = await h,
    { name: o, icon: i } = await g;
  console.log('A', s, r), console.log('B', o, i);
})();
console.log('--- ENTRY END ---');

/*
 * chunkA.js
 */
import { f as o } from './faCat.6085f215.js';
const l = {};
(function (a) {
  Object.defineProperty(a, '__esModule', { value: !0 });
  const C = 'fal',
    c = 'dog',
    n = 576,
    e = 512,
    i = [128021],
    f = 'f6d3',
    v = '...icon...';
  (a.definition = { prefix: C, iconName: c, icon: [n, e, i, f, v] }),
    (a.faDog = a.definition),
    (a.prefix = C),
    (a.iconName = c),
    (a.width = n),
    (a.height = e),
    (a.ligatures = i),
    (a.unicode = f),
    (a.svgPathData = v),
    (a.aliases = i);
})(l);
(function (a) {
  Object.defineProperty(a, '__esModule', { value: !0 });
  const C = 'fal',
    c = 'cat',
    n = 576,
    e = 512,
    i = [128008],
    f = 'f6be',
    v = '...icon...';
  (a.definition = { prefix: C, iconName: c, icon: [n, e, i, f, v] }),
    (a.faCat = a.definition),
    (a.prefix = C),
    (a.iconName = c),
    (a.width = n),
    (a.height = e),
    (a.ligatures = i),
    (a.unicode = f),
    (a.svgPathData = v),
    (a.aliases = i);
})(o);
console.log('--- File A START ---');
console.log('A', l.faDog, o.faCat);
const L = 'I am A',
  d = l.faDog;
console.log('--- File A END ---');
export { d as icon, L as name };

/*
 * chunkB.js
 */
import { f as o } from './faCat.6085f215.js';
console.log('--- File B START ---');
console.log('B', o.faCat);
const n = 'I am B',
  t = o.faCat;
console.log('--- File B END ---');
export { t as icon, n as name };

/*
 * faCat.js
 */
const a = {};
// This line should contain one of the icon definition function calls that is currently in chunkA.js
export { a as f };

Source code to above output

/*
 * index.js
 */
console.log('--- ENTRY START ---');
// import {name as testAName, icon as testAIcon} from './testA';
// import {name as testBName, icon as testBIcon} from './testB';
const testA = import('./testA');
const testB = import('./testB');

(async () => {
  const {name: testAName, icon: testAIcon} = await testA;
  const {name: testBName, icon: testBIcon} = await testB;

  console.log('A', testAName, testAIcon);
  console.log('B', testBName, testBIcon);
})();
console.log('--- ENTRY END ---');

export const HelloThere = 'HelloThere';


/*
 * a.js
 */
console.log('--- FILE A START ---');
import { faDog } from '@fortawesome/pro-light-svg-icons/faDog';
import { faCat } from '@fortawesome/pro-light-svg-icons/faCat';

console.log('A', faDog, faCat);

export const name = 'I am A';
export const icon = faDog;
console.log('--- FILE A END ---');


/*
 * b.js
 */
console.log('--- FILE B START ---');
import { faCat } from '@fortawesome/pro-light-svg-icons/faCat';

console.log('B', faCat);

export const name = 'I am B';
export const icon = faCat;
console.log('--- FILE B END ---');

Importing via index (esm)

Destructured Import

Import Documention Index.es.js

Importing the icons via import {faCat, faDog} from @fortawesome/pro-light-svg-icons which results in all icons across the app being placed into a single chunk even if they're being a dynamic import (e.g. A page component), not ideal for download size or performance. However does resolve the missing icon problem described above as all icons are loaded in one place, and this chunk is one of the first to load as it's needed almost everywhere.

Importing files directly (commonjs)

Config

Work around

Currently the only work around I've found to avoid the performance impact for the second example is to define all icon imports in the manualChunks config of vite.config.ts, which pulls each icon into it's own chunk. Not great but does solve the issue bar having a few hundred chunks that each contain one icon.


Solution

  • Did manage to find a solution to this with help from the rollup discord and a lot of digging through vite. Seems like the rollup commonjs plugin doesn't hoist fontawesome icons correctly in this use case (Direct Import).

    The solution was to pass strictRequires option to commonjs for all icon imports. Both options below worked in my case however the side-effects of either I have yet to test.

      // vite.config.js
      build: {
        commonjsOptions: {
          strictRequires: [
            new RegExp('@fortawesome\/[\\w]+-[\\w]+-svg-icons\/fa[\\w]+\.js')
          ]
        }
      }
    
      // vite.config.js
      build: {
        commonjsOptions: {
          strictRequires: true
        }
      }