Search code examples
javascriptrollup

Importing a whole directory of static files in Rollup.js


I am developing a TypeScript library and want to migrate the build process from webpack to rollup.

In my project, I have a directory of SVG icons, sorted into several sub-directories. My library should export a JavaScript object that contains the SVG code of all these icons.

With webpack, I was using require.context to recursively require the SVG files (which were configured as asset/source):

const context = require.context("../../assets/icons");
export const icons: Record<string, Record<string, string>> = {};
for (const key of context.keys() as string[]) {
    const [set, fname] = key.split("/").slice(-2);

    if (!icons[set])
        icons[set] = {};

    icons[set][fname.replace(/\.svg$/, "")] = context(key);
}

How can I achieve this with rollup?

I have had several ideas so far:

  • rollup-plugin-require-context is supposed to bring require.context into rollup. However, it seems to be buggy and unmaintained, and in my tests it did not transform the require.context call at all.
  • rollup-plugin-glob-import seems to be suitable for recursively importing a directory. I was trying to use it along with rollup-plugin-string to import the SVG files as plain text. However, it seems like the two plugins don’t communicate with each other, no matter in which order I define them. The glob import plugin complains that my SVG files do not have any exports.

Solution

  • Solution 1: rollup-plugin-glob-import

    Using rollup-plugin-glob-import in combination with rollup-plugin-string works when configured in the right way.

    rollup-plugin-string generates modules with a string as its default export, so format: 'default' has to be used for rollup-plugin-glob-import to import them properly. If sub directory paths should be preserved in the named exports of the resulting bundle, a custom rename function can be used (note that the export name needs to be a valid JavaScript variable name, hence the use of camelCase here):

    import { defineConfig } from 'rollup';
    import globImport, { camelCase } from 'rollup-plugin-glob-import';
    import { string } from 'rollup-plugin-string';
    
    export default defineConfig({
        ...
        plugins: [
            string({
                include: "assets/icons/**/*.svg"
            }) as any,
            globImport({
                format: 'default',
                rename: (name, id) => name || `${camelCase(basename(dirname(id)))}_${camelCase(basename(id, extname(id)))}`
            })
        ]
    });
    

    rollup-plugin-glob-import generates named exports but no default export, so the exported files can be imported like this:

    import * as icons from '../../assets/icons/**/*.svg';
    

    Solution 2: Custom plugin

    Writing a custom Rollup plugin is not difficult and allows for more flexibility. In particular, it can be used to export a single JSON object rather than one named export per file (whose name is restricted by the allowed characters for JavaScript variable names).

    In this example I'm providing a module called custom:icons, which has a default export containing all the files:

    import { defineConfig } from "rollup";
    import glob from 'fast-glob';
    import { readFile } from 'fs/promises';
    
    export default defineConfig({
        ...
        plugins: [
            {
                name: 'custom:icons',
                resolveId: (id) => {
                    if (id === 'custom:icons') {
                        return id;
                    }
                },
                load: async (id) => {
                    if (id === 'custom:icons') {
                        const icons: Record<string, Record<string, string>> = {};
                        for (const path of await glob('./assets/icons/*/*.svg')) {
                            const [set, fname] = path.split("/").slice(-2);
    
                            if (!icons[set])
                                icons[set] = {};
    
                            icons[set][fname.replace(/\.svg$/, "")] = (await readFile(path)).toString();
                        }
                        return `export default ${JSON.stringify(icons, undefined, '\t')}`;
                    }
                }
            }
        ]
    });
    

    The custom module can then be imported like this:

    import icons from 'custom:icons';