Use "external" compilers in Bud

Hello,

we need to implement a custom element built with Svelte into our Sage 10 website and compile it from Sage with bud.

I’ve installed the svelte package with yarn and moved the .svelte files into my scripts folder but I can’t find how to implement custom commands into Sage 10’s default bud configuration.

I tried calling svelte with the bud.get() method like this:

app.get('svelte').entry({
  demo: ['scripts/components/demo-kt/main.js'],
})

but to no avail.

Looking at Bud’s documentation the bud.make() method looks more helpful but the docs aren’t that detailed towards external libraries.

Anyone has implemented something like that yet?

1 Like

Hi there,

I tried using the “make” like this after reading the multi-compilers page:

.make('svelte', svelte => {
  svelte
  .use(require('@svelte/compiler.d.ts'))
  .setPath('svelte', 'public/svelte')
  .entry('demo-kt', ['scripts/components/demo-kt/main.js'])
})

but I realized that the “svelte” command / task that should be used on the source files is never specified so I supposed the make method only works with extensions made for bud.

Is there a way to specify a node command on certain files or should I try to build a custom extension?

Thanks

1 Like

Hey @GiulioPecorella . did you ever figure this out? I would like to use Svelte when developing with Sage10, but I have not figured out how to include it properly yet… So I’ll just follow on this thread, hoping someone can shed some light on how to include it.

At the moment I’ve gone back to Vue, it took a little longer but it did the work but it’s not a solution to the issue.

OK, thanks for letting me know… If I can ask you something out of curiosity, are you using Vue 2 or the lates Vue 3? the release of Vue3 messed up my workflow pretty bad. I had a (IMO) pretty neat setup with Vue2 where I mounted Vue as a wrapper to entire page #app, and using Components where I needed them, this worked very well with Sage10, with the use of slots etc. I have not been able to achieve this workflow with Vue3… This was the reason I was looking into Svelte in the first place, as it seem to mimick the workflow I wanted.

So my question is really if you are using Vue3 components?

Yup, it’s Vue3 and actually worked like a charm with Bud (even though I had to downgrade everything to HTTP until the HTTPS issue is fixed)

Good news, everyone, SSL is available in 5.5.0.

Here’s how you would handle adding support for .svelte to bud.js. I’m going to do this in a couple parts. Hopefully someone might consider taking the end result and packaging it up as the first official bud community supported extension :roots:?

Dependencies

Install svelte and svelte-loader to your project.

Handling .svelte

First off, we need to add support for .svelte files if Bud doesn’t already support them.

We can scope what Bud knows about with the build.resolve.extensions hook:

app.log(
  app.hooks.filter('build.resolve.extensions')
).close()

Run yarn bud build --log and you’ll see:

[root] ❯ [
  '.wasm',  '.mjs',
  '.js',    '.jsx',
  '.css',   '.json',
  '.json5', '.toml',
  '.xml',   '.csv',
  '.yml',   '.yaml',
  '.xml'
]

So, confirmed that Bud doesn’t know what .svelte is. So, we’ll add the extension using the same hook:

app.hooks.on(
  'build.resolve.extensions', 
  (extensions) => [...extensions, '.svelte']
)

Overriding the configuration

We want to add the svelte loader using the build.module.rules.oneOf hook:

app.hooks.on('build.module.rules.oneOf', (rules) => [
  ...rules,
{
  test: /\.svelte$/,
  use: [
    {
      loader: 'svelte-loader',
      options: {},
    },
  ],
})

Final config

That’s that! Here’s the final config:

/**
 * @typedef {import('@roots/bud').Bud} bud
 * @param {bud} app
 */
module.exports = async (app) => {
  /**
   * Add svelte support
   */
  app.hooks
    .on('build.module.rules.oneOf', (rules) => [
      ...rules,
      {
        test: /\.svelte$/,
        use: [
          {
            loader: 'svelte-loader',
            options: {},
          },
        ],
      },
    ])
    .hooks.on('build.resolve.extensions', (ext) => [...ext, '.svelte'])

    /**
     * Application entrypoints
     *
     * Paths are relative to your resources directory
     */
    .entry({
      app: ['@scripts/app', '@styles/app'],
      editor: ['@scripts/editor', '@styles/editor'],
    })

    /**
     * These files should be processed as part of the build
     * even if they are not explicitly imported in application assets.
     */
    .assets('images')

    /**
     * These files will trigger a full page reload
     * when modified.
     */
    .watch('resources/views/**/*', 'app/**/*')

    /**
     * Target URL to be proxied by the dev server.
     *
     * This is your local dev server.
     */
    .proxy('http://example.test')

    /**
     * Development URL
     */
    .serve('http://example.test:3000')

    /**
     * Generate WordPress `theme.json`
     *
     * @note This overwrites `theme.json` on every build.
     */
    .themeJson()

    /**
     * Set `theme.json` colors from `tailwind.config.js` values
     */
    .useTailwindColors();
};

Associated docs

4 Likes

Adding support for svelte through Bud’s APIs

The above instruction work great for quickly adding to a single project but if you want to share it it’s less than ideal. There is no way to pass options in, or any simple way to modify the rule once it has been set.

Adding support for svelte with the @roots/bud-build API is not a big deal and solves almost all of those problems.

Handling .svelte

We still need to do this and the hook is still the best way to get it done.

app.hooks.on(
  'build.resolve.extensions', 
  (extensions) => [...extensions, '.svelte'],
)

Registering the svelte-loader

Import the Loader class at the top of the config:

const {Loader} = require('@roots/bud')

And now register the loader:

app.build.loaders.svelte = new Loader('svelte-loader');

Creating a “use” for the svelte-loader

Import the Item class at the top of the config:

const {Item} = require('@roots/bud')

And register the loader item:

app.build.items.svelte = new Item({
  loader: app.build.loaders.svelte,
  options: {},
});

There are plenty of options you can use to configure svelte-loader. Check out the svelte-loader README. You can place options in the options property.

Creating a Rule to “use” the loader

Rule is kind of fancy and has a helper to make it easy to add, so you don’t need to require Rule from @roots/bud-build. One day Item and Loader will get the same treatment.

app.build.setRule('svelte', {
  test: /\.svelte$/,
  use: [app.build.items.svelte],
});

Final config

const {Loader, Item} = require('@roots/bud-build');

/**
 * @typedef {import('@roots/bud').Bud} bud
 * @param {bud} app
 */
module.exports = async (app) => {
  /* Add `.svelte` extension */
  app.hooks.on('build.resolve.extensions', (ext) => [...ext, '.svelte']);

  /* Add svelte loader */
  app.build.loaders.svlete = new Loader('svelte-loader');

  /* Add svelte item */
  app.build.items.svelte = new Item({
    loader: app.build.loaders.svlete,
    options: {},
  });

  /* Add svelte rule */
  app.build.setRule('svelte', {
    test: /\.svelte$/,
    use: [app.build.items.svelte],
  });

  /**
   * Application entrypoints
   *
   * Paths are relative to your resources directory
   */
  app
    .entry({
      app: ['@scripts/app', '@styles/app'],
      editor: ['@scripts/editor', '@styles/editor'],
    })

    /**
     * These files should be processed as part of the build
     * even if they are not explicitly imported in application assets.
     */
    .assets('images')

    /**
     * These files will trigger a full page reload
     * when modified.
     */
    .watch('resources/views/**/*', 'app/**/*')

    /**
     * Target URL to be proxied by the dev server.
     *
     * This is your local dev server.
     */
    .proxy('http://example.test')

    /**
     * Development URL
     */
    .serve('http://example.test:3000')

    /**
     * Generate WordPress `theme.json`
     *
     * @note This overwrites `theme.json` on every build.
     */
    .themeJson()

    /**
     * Set `theme.json` colors from `tailwind.config.js` values
     */
    .useTailwindColors();
};

What makes it better

It’s arguably easier to read and maintain, but other than that you have retained the ability to modify all of the above. It’s no longer a one way ticket.

For instance, now you can set different options after the fact:

app.build.items.svelte.setOptions({
  emitCss: true,
})

Or add additional directories to look for modules in:

app.build.rules.svelte.setInclude(['node_modules', 'src'])

It’s this extensibility that makes it ideal for sharing and what makes it possible for others to use it in their own extensions.

Associated documentation

3 Likes

Making an extension

Here’s where we left off:

const {Loader, Item} = require('@roots/bud-build');

/**
 * @typedef {import('@roots/bud').Bud} bud
 * @param {bud} app
 */
module.exports = async (app) => {
  /* Add `.svelte` extension */
  app.hooks.on('build.resolve.extensions', (ext) => [...ext, '.svelte']);

  /* Add svelte loader */
  app.build.loaders.svelte = new Loader('svelte-loader');

  /* Add svelte item */
  app.build.items.svelte = new Item({
    loader: app.build.loaders.svlete,
    options: {},
  });

  /* Add svelte rule */
  app.build.setRule('svelte', {
    test: /\.svelte$/,
    use: [app.build.items.svelte],
  });

  // ... rest of config
};

Let’s wrap up all the svelte stuff in an extension:

We’ll start by dropping everything into a new file: bud.svelte.js. We’ll put the stuff that was in the config function into a new function called register:

const { Loader, Item } = require("@roots/bud-build");

/**
 * Register svelte extension
 */
const register = (app) => {
  app.hooks.on('build.resolve.extensions', (ext) => [...ext, '.svelte']);

  app.build.loaders.svelte = new Loader('svelte-loader');

  app.build.items.svelte = new Item({
    loader: app.build.loaders.svlete,
    options: {},
  });

  app.build.setRule('svelte', {
    test: /\.svelte$/,
    use: [app.build.items.svelte],
  });
};

Now we’ll give our extension a name:

const name = `bud-svelte`

Lastly, we export name and register:

module.exports = {name, register}

Final extension

const { Loader, Item } = require("@roots/bud-build");

/**
 * Extension name
 */
const name = `bud-svelte`

/**
 * Extension registration
 */
const register = (app) => {
  app.hooks.on('build.resolve.extensions', (ext) => [...ext, '.svelte']);

  app.build.loaders.svelte = new Loader('svelte-loader');

  app.build.items.svelte = new Item({
    loader: app.build.loaders.svlete,
    options: {},
  });

  app.build.setRule('svelte', {
    test: /\.svelte$/,
    use: [app.build.items.svelte],
  });
};

module.exports = {name, register}

Using the extension

With our extension file all set up we can now require it as a module and register it with bud.use.

const svelte = require("./bud.svelte");

module.exports = async (app) => {
  /**
   * Add svelte support
   */
  app.use(svelte)

    /**
     * ...rest
     */
  app.entry({
      app: ["@scripts/app", "@styles/app"],
      editor: ["@scripts/editor", "@styles/editor"],
  })

 // ...
};

Extensions which are included in the project package.json don’t need to have use called on them. It’s done automatically. But since our package is just a single file in the project directory and not listed as a project dependency, this is how it’s done.

Associated documentation

API documentation for extensions:

Documentation on bud.use:

2 Likes

Packaging it as an extension

I mean, why not? We’ll be famous.

Our package.json:

{
  "name": "bud-svelte",
  "bud": {
    "type": "extension"
  },
  "files": ["./bud.svelte.js"],
  "main": "./bud.svelte.js",
  "dependencies": {
    "svelte": "^3.46.4",
    "svelte-loader": "^3.1.2"
  }
}

The important bit is the bud.type part. That’s how bud.js knows it’s dealing with a bud extension.

Add the bud.svelte.js file from the post above and you’re (probably) done!

Now, anybody with a bud project can just install bud-svelte and be off to the races.

If anybody actually publishes this

Let me know and I’ll find some way to promote it on the site.

4 Likes

hey @kellymears thanks for this! :pray:

Having a little trouble adding import-glob-loader for scss imports, so we can do this:

// app.scss
...
@import "4-components/*";
...

Our previous mix config:

// webpack.mix.js
mix
  .webpackConfig({
    devtool: 'source-map',
    module: {
      rules: [
        {
          test: /\.scss/,
          loader: 'import-glob-loader',
        },
      ],
    },
  });

What I’ve tried with Bud so far:

// bud.config.js
module.exports = async (app) => {
  // Add scss glob import support
  app.hooks
    .on('build.module.rules.oneOf', (rules) => [
      ...rules,
      {
        test: /\.scss$/,
        use: [
          {
            loader: 'import-glob-loader',
            options: {},
          },
        ],
      },
    ])
    .hooks.on('build.resolve.extensions', (ext) => [...ext, '.scss']);

But get the following error.

Any help is much appreciated as always :slight_smile:

So got this working in the most brittle/hackiest/jankiest way possible! haha

  // Add scss glob import support
  app.hooks.on('build.module.rules.oneOf', (rules) => {
    const scssConfig = rules.splice(-1, 1)[0];

    scssConfig.use.push({
      loader: 'import-glob-loader',
    });

    rules.push(scssConfig);
    return rules;
  });

But has helped me kinda understand the issue a little. I need to insert import-glob-loader into @roots/bud-sass config somehow. Not sure if this is currently possible with Bud hook or filter?

Hoping someone has a nicer way to get this working than this :crossed_fingers:

@slowrush is there any reason you can’t just do the glob in bud.config.js?

app.entry({
  app: ['4-elements/*', 'app.scss']
})

sorry if i don’t understand something scss related as to why that isn’t viable…

in 5.6.0 – which will release very, very soon – the api for all of this has been expanded. this is, internally, how @roots/bud-sass handles adding sass:

    app.hooks
      .on('build.resolve.extensions', ext => ext.add('.scss'))
      .build.setLoader('sass', require.resolve('sass-loader'))
      .setItem('sass', {
        loader: 'sass',
        options: {implementation, sourceMap: true},
      })
      .setRule('sass', {
        test: app => app.store.get('patterns.sass'),
        include: app => [app.path('@src')],
        use: [`precss`, `css`, `postcss`, `resolveUrl`, `sass`],
      })

So, in 5.6.0 you should be able to register your loader and a RuleSetUseItem for it:

app.build
  .setLoader('import-glob', require.resolve('import-glob-loader'))
  .setItem('import-glob', {loader: 'import-glob'})

And then add it to the list of loaders for sass:

app.build.rules.sass.setUse(items => [...items, `import-glob`])
2 Likes

hey @kellymears that’s a good point! I’m not super familiar with how webpack deals with entrypoint imports but will give that a test a little later to see if it compiles the scss in the order we need it to.

The 5.6.0 release looks great! thanks again for all the effort that’s gone into Bud, it’s pretty slick! :raised_hands:

For context here’s a better look at our scss structure - we import in specificity order ie. ITCSS. Hopefully, your suggestion will “just work” otherwise we’ll use the new loader API + import-glob-loader.

resources/
├─ styles/
│  ├─ 1-settings/
│  │  ├─ _index.scss
│  │  ├─ _colours.scss
│  │  ├─ _grid-settings.scss
│  │  ├─ _z-indexes.scss
│  ├─ 2-tools/...
│  ├─ 3-base/...
│  ├─ 4-elements/*
│  │  ├─ _figure.scss
│  │  ├─ _forms.scss
│  │  ├─ _hr.scss
│  │  ├─ _img.scss
│  │  ├─ _links.scss
│  │  ├─ _lists.scss
│  │  ├─ _table_scss
│  ├─ 5-structure/*
│  ├─ 6-plugins/*
│  ├─ 7-components/*
│  ├─ 8-wp-core-blocks/*
│  ├─ 9-pages/*
│  ├─ 10-utils/*
│  ├─ 11-cheats.scss
│  ├─ app.scss
│
│
├─ blocks/ (imported per ACF Block, individual css + js files generated)
│  ├─ hero/
│  │  ├─ hero.js
│  │  ├─ hero.scss

// app.scss

// Settings - colours, breakpoints
@import "1-settings";

// Tools - @media-breakpoint-up('md), z('high')
@import "2-tools";

// Generic / Resets - html, body, box-sizing
@import "3-base";

// Elements - h1, img
@import "4-elements/*";

// Objects/Structure - grid, containers, columns
@import "5-structure/*";

// Plugins - Gravity Forms, Swiper.js etc
@import "6-plugins/*";

// Custom Components - Nav, Cards, etc
@import "7-components/*";

// Blocks - wp-block-columns, buttons etc
@import "8-wp-core-blocks/*";

// Theme/Page specific styles - home, about, contact
@import "9-pages/*";

// Utils - u-p-0, u-hide, u-text-upper
@import "10-utils/*";

// Having a bad day?
// ...relax... put your temporary fixes here
// only if you promise to refactor them later!
@import "11-cheats";
1 Like

Just noting that the syntax needs to change for Bud 6:

bud.config.mjs

app.build
  .setLoader('import-glob', await bud.module.resolve('import-glob'))

FWIW this also worked but I feel like the syntax is a little confusing:

app.build
  .setLoader('import-glob', 'import-glob')

More details here https://bud.js.org/blog/6.0.0#notes-on-import-vs-require-in-the-context-of-budjs

3 Likes

I’m trying migrate to esm (from bud 5.7.7 to 6.3.3). When I try to apply this config:

.hooks.on('build.module.rules.oneOf', (rules) => [
  ...rules,
  {
    test: /\.(glsl|vs|fs|vert|frag)$/,
    use: [
      {
        loader: 'raw-loader',
        options: {},
      },
    ],
  },
  {
    test: /\.css$/, use: 'css-loader',
  },
])
.hooks.on('build.resolve.extensions', (ext) => [...ext, '.glsl']) // <------ERROR IN THIS LINE

I get an error:

[bud@6.3.3] [bud] › ✖  TypeError: ext.add is not a function 
    at file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-sass/lib/extension.js:46:66
    at file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-hooks/lib/service.js:176:80
    at Array.reduce (<anonymous>)
    at Hooks.filter (file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-hooks/lib/service.js:176:47)
    at file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-build/lib/config/builder.js:86:49
    at async file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-hooks/lib/service.js:202:42
    at async Hooks.filterAsync (file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-hooks/lib/service.js:200:24)
    at async file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-build/lib/service.js:63:19
    at async Promise.all (index 2)
    at async Build.make (file:///Users/aitor/Documents/Trellis/angelgiraldez.com/site/web/app/themes/sage/node_modules/@roots/bud-build/lib/service.js:34:9)

Commenting the last line, it woks. Could you provide an adaptation of that config to msj? Thanks.

@aitor, in 6.x extensions were converted to be a Set rather than an Array

Try changing your last line to:

bud.hooks.on('build.resolve.extensions', (ext) => ext.add('.glsl'))

Sorry for the API change but it’s for the best because now we don’t have to worry about duplicative extensions.

3 Likes

I appreciate very much all your work in Bud. Really, I love it. Thanks for that.

1 Like

@kellymears I hope you are doing good. I am trying to use import-glob-loader with Bud v6.6.5.
I tried to configure it as above but was not successful. Could you please provide an example how I could add import-glob-loader to run with Bud and the latest API changes?

Thank you so much and kind regards,

Philipp

Sorry for the delayed response. Holidays! Releases!

Not much has changed. This is pretty accurate: Customizing loaders | bud.js

I haven’t looked at import-glob-loader closely enough to know how to specifically advise, but the following pattern applies to all loaders

1. Add the loader

you don’t need to try and resolve the loader if it is a direct dependency anymore. you can just pass the package signifier and be done:

bud.build.setLoader(`import-glob-loader`)

2. Create a RuleSetItem with it

if there are no options to pass you can leave it out:

bud.build.setItem(`import-glob-loader`, {
   loader: `import-glob-loader`,
   options: {},
})

3. Modify a rule to use the RuleSetItem

maybe it applies to js:

bud.build.rules.js.setUse((items = []) => [
  ...items,
  bud.build.items[`import-glob-loader`]
])

if css:

bud.build.rules.css.setUse((items = []) => [
  ...items,
  bud.build.items[`import-glob-loader`]
])
2 Likes