Output sass and js files to their own subdirectories in the public folder

I have a Sage 10 theme (v10.6.0), and am using Bud (v6.12.2) as the build tool.

I am using bootstrap with sass, and have got it all working fine by following the guide in the sage docs.

My folder structure is like so:

- resources
  - styles
    app.scss
    - blocks
      - block-a.scss
      - block-b.scss
    - components
      - component-a.scss
      - component-b.scss

I’d like it to build and output like so:

- public
  - css
    app.css
    - blocks
      - block-a.css
      - block-b.css
    - components
      - component-a.css
      - component-b.css

The reason I’m doing this is so that I can enqueue the assets for my blocks and components only if they are being used on the page, while keeping everything a bit neater.

I’ve already come up with something that gets me part the way there: the below will grab all the scss files and js files from the styles/block folder and the scripts/blocks folder and will output them individually to the public/css and public/js folders, but I’d really like them to go into a ‘blocks’ subfolder within each of those css and js output directories. Then the same for the components folder (which I havent done yet, just want to get blocks working first).

my asset mapper (bud.config.js):

    const blockFiles = {};

    const processBlockFiles = async (filePattern, importPrefix) => {
        const files = await app.glob(filePattern);
        files.forEach((file) => {
            const name = path.basename(file, path.extname(file));
            const type = path.extname(file).slice(1);
            blockFiles[name] = blockFiles[name] || { import: [], type };
            blockFiles[name].import.push(`${importPrefix}/${name}`);
        });
    };

    await processBlockFiles("resources/styles/blocks/*.scss", "@styles/blocks");
    await processBlockFiles("resources/scripts/blocks/*.js", "@scripts/blocks");

how it’s used (bud.config.js):

app.entry({
        app: ["@scripts/app", "@styles/app"],
        editor: ["@scripts/editor", "@styles/editor"],
        ...blockFiles,
    }).assets(["images"]);

I’ve read the great post below, but what I’m doing is a little different. I played around with some variants of it, but have gone in enough circles where I’m now reaching out
https://discourse.roots.io/t/bud-config-for-outputting-multiple-css-files-from-scss-for-acf-blocks/22498/6?u=izzyzzi

Any help would be greatly appreciated!

I solved this by using a separate entrypoint (in the bud config). Each entrypoint can have it sown styles and scripts and can be enqueued as a bundle.

Thanks for the tip @strarsis ! (I love a lot of your contribs on here) That’s a great idea for the enqueuing using bundle, I might take a look at that.

I assume that you are outputting each script and stylesheet just to the default public directory? ie. public/css and public/js. Or are you building to a separate top level folder (like public/blocks)?

My kingdom for someone who can solve my riddle of outputting the block sass files individually to public/css/blocks/ and the same for js to public/js/blocks/

bud.config.js:

    .entry('app', ['@scripts/app', '@styles/app'])
    .entry('editor', ['@scripts/editor', '@styles/editor'])
	
    .entry('custom-block', ['@scripts/custom-block', '@styles/custom-block'])

(e.g. setup.php)

add_action('wp_enqueue_scripts', function () {
    if (!has_block('themenamespace/custom-block')) {
        return; // skip
    }

    bundle('custom-block')->enqueue();
}, 100);
1 Like

Appreciate the guidance on the enqueueing @strarsis

I’ll be sure to update this post if I end up finding a solution to having subfolders in the output folder, as I’m sure others may find it useful too. If anyone has any ideas though, let me know :slight_smile:

(bud.entry | bud.js):

The entire EntryObject API is available to you.

So, in theory, you should be able to specify a subdirectory using the filename option field.

See these webpack examples (bud abstracts webpack):
https://stackoverflow.com/a/56289959/4150808

I have previously tried something similar. From Bud’s own docs I followed their example of specifying the options (import, dependOn etc.).

Bud’s docs:

bud.entry({
    react: ['react', 'react-dom'],
    app: {
      import: ['app.js', 'app.css'],
      dependOn: ['react'],
      publicPath: 'https://cdn.example.com/app/',
    },
  })

I gave the following a try, and it compiles, but will still just compile to the public/css/accordion.css. It did not change the path or the filename, it still just used the entry point item’s name. It’s almost like there is something that is overriding anything you specify for the output?

// Builds to public/css/accordion.css
app.entry({
        accordion: {
            import: ["styles/components/c-accordion.scss"],
            filename: "test/test.css",
        },
    });

Indeed, the output.filename field is dynamically populated:

There is an example for a bud hook that allows changing the filename:
https://github.com/roots/bud/blob/70c4114d5738200721679cee98ec31a7c4d9de3f/sources/%40roots/bud-hooks/src/service.ts#L30

Edit: Note that the filter function are an analog to what WordPress does. You need to hook into the filter in order to change the value that is filtered in bud:
https://bud.js.org/reference/bud.hooks#putting-it-together

bud.hooks.on(`build.output.filename`, filename => {
  // custom filename path logic
    return filenameWithSubdirectory;
   // or return filename unchanged
    return filename;
})

Path manipulation should be easy using NodeJS built-in path functions:

import 'path' from 'node:path';

const directoryPath = path.dirname(filename);
const filenamePath = path.basename(filename, path.extname(filename));
const filenameWithSubdirectory = path.join(directoryPath, 'custom-subdirectory', filenamePath);
return filenameWithSubdirectory;

The harder part is actually determining what filename is meant for what entrypoint. Is additional context available (e.g. this) or can some passed to that filter?
In your case, it may suffice to use the name of the file (path.basename(filename)) for determining what entrypoint the filtered filename is used for.

const filenameBasename = path.basename(filename);
if(filenameBasename === 'custom-block') { // there may be some additonal cruft to be removed or regexp'ed against
    // custom subdirectory should be 'custom-block/' then
}

Edit; bud uses a “magic string” for the filename, this could be an opportunity even better hooking:
https://github.com/roots/bud/blob/70c4114d5738200721679cee98ec31a7c4d9de3f/sources/%40roots/bud-framework/src/methods/path.ts#L61

2 Likes

@strarsis Thank you so much for your detailed response, I deeply appreciate the time and effort you took to help me out.

I went in circles a few times trying to get the hooks to work - then out of the blue I thought that maybe I can try again to edit the way I was originally trying to do it - and simply prefix the entrypoint’s name with the subfolder i wanted to output it to, and it seems to have worked (in my case).

Here is the code that seems to be working for me:

const assetFiles = {};

    const processFiles = async (filePattern, importPrefix, entryPrefix) => {
        const files = await app.glob(filePattern);
        files.forEach((file) => {
            const name = path.basename(file, path.extname(file));
            const type = path.extname(file).slice(1);
            // Use entryPrefix + name as the entry name
            const entryName = `${entryPrefix}/${name}`;
            assetFiles[entryName] = assetFiles[entryName] || { import: [], type };
            assetFiles[entryName].import.push(`${importPrefix}/${name}`);
        });
    };

    await processFiles("resources/styles/blocks/*.scss", "@styles/blocks", "blocks");
    await processFiles("resources/scripts/blocks/*.js", "@scripts/blocks", "blocks");
    await processFiles("resources/styles/components/*.scss", "@styles/components", "components");
    await processFiles("resources/scripts/components/*.js", "@scripts/components", "components");

    app.entry({
        app: ["@scripts/app", "@styles/app"],
        editor: ["@scripts/editor", "@styles/editor"],
        ...assetFiles,
    }).assets(["images"]);

This grabs for example the component ‘accordion’ scss and js files from their respective resources folder and consolidates them under one entrypoint name, and outputs to public/css/components/accordion.css and public/js/components/accordion.js

And it will do the same thing for any blocks you have and output to public/css/blocks/.css for example. I haven’t rigorously tested yet, but will update this thread if I have any issues.

Hope this can help somebody else, and thanks again @strarsis

1 Like