Skip to content

Getting Started

Neodymium edited this page Dec 10, 2024 · 11 revisions

Installation

To install BundleBD, simply create a new folder, open it in your preferred editor/terminal, and run:

npm i bundlebd -D

Basics

First make sure you read the BetterDiscord Plugin Docs and know at least the basics of plugin development.

Basic Usage

By default, the bundler will look in the src directory for the plugin's files. Create a new src folder, and inside of it create an index.js file, exporting a simple BetterDiscord Plugin without the meta. For example:

// src/index.js

export default class MyAmazingPlugin {
	start() {
		console.log("Plugin started");
	}

	stop() {
		console.log("Plugin stopped");
	}
}

Then, create a manifest.json file in the same folder as your plugin's main file. In this configuration file, add information about the plugin, like it's name, author, description and version:

// src/manifest.json

{
	"name": "MyAmazingPlugin",
	"author": "Neodymium",
	"description": "A plugin that does absolutely nothing",
	"version": "1.0.0"
}

(Note: The name in the plugin configuration will also be stripped of any whitespace and used as the bundled plugin's filename.)

The only required fields are name, author, description, and version, but you can add other information as well. For all configuration options, see Plugin Configuration.

Now you can run...

npx bundlebd

...in the terminal to bundle the plugin. The bundler will, by default, place the bundled plugin in the dist folder, creating it if necessary.

Multiple Files/Modules

Of course, the real appeal of using a bundler is the ability to bundle multiple modules into one plugin file. To use multiple files/modules, just use Javascript's module syntax. As an example, let's say there are two files in the src folder with the following contents:

// src/index.js

import { helloWorld } from "./utils";

export default class MyAmazingPlugin {
	start() {
		helloWorld();
	}

	stop() {
		console.log("Plugin stopped");
	}
}
// src/utils.js

export function helloWorld() {
	console.log("Hello world!");
}

Now, when the bundler sees that the plugin's main file imports from utils.js, it will bundle it into the plugin.

Using Typescript

Using Typescript is very simple. Just include a Typescript file in your plugin, and the bundler will automatically transpile it for you. For example, the following will result in a plugin similar to the previous example, with no additional configuration:

// src/index.ts

import { hello } from "./utils";

export default class MyAmazingPlugin {
	start() {
		let message: string;
		message = hello("Neodymium");
		console.log(message);
	}

	stop() {
		console.log("Plugin stopped");
	}
}
// src/utils.ts

export function hello(name: string): string {
	return `Hello ${name}!`;
}

Typings

To resolve issues with typings for BdApi and other utilities, see here.

Using JSX

Just like Typescript, JSX is very easy to use. Just include some JSX elements in your code, and the bundler will automatically transpile them into React.createElement calls. For example:

// src/index.jsx

export default class MyAmazingPlugin {
	start() {
		const element = <div className="class">Hello World!</div>;
		console.log(element);
	}

	stop() {
		console.log("Plugin stopped");
	}
}

With Typescript

Using JSX alongside Typescript is still simple, but requires some additional thought.

First, if you use JSX in Typescript, make sure the file has a .tsx extension. Not doing so will result in errors, and the plugin will not bundle successfully.

Additionally, you may get a warning along the lines of:

'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.

You can safely ignore this warning, as the bundler will include a reference to Discord's React instance in the bundled plugin. However, there are two ways to get rid of it. The first is to include...

import React from "react";

...at the top of your file. The second is to create a tsconfig.json file in your project's root directory with the following contents:

{
	"compilerOptions": {
		"jsx": "react-jsx"
	}
}

See Typescript Configuration for more recommended TSConfig options.

Using BdApi

Utilizing functions and utilities from BdApi is easy, just import them from betterdiscord. BundleBD will also automatically create a bound BdApi instance with the plugin's name, so there's no need to use callers or IDs!

// src/index.js
import { Webpack } from "betterdiscord";

export default class MyAmazingPlugin {
	start() {
		const UserPopoutBody = Webpack.getModule((m) => m.default?.displayName === "UserPopoutBody");
		console.log(UserPopoutBody);
	}

	stop() {}
}

(Note: The BdApi global is also still available)

The bundler includes typings/autocomplete for BdApi. If they are not being detected in Typescript, see here.

Thanks to Zerthox for the BdApi typings!

Using Stylesheets

As should be expected by this point, stylesheets are also easy to use. Importing a stylesheet will give you a string of the stylesheet's processed CSS, which you can then inject like normal CSS. Local @imports and URLs included in imported stylesheets will also be bundled with the plugin.

// src/index.js

import styles from "./index.css";
import { DOM } from "betterdiscord";

export default class MyAmazingPlugin {
	start() {
		DOM.addStyle(styles);
		console.log("Plugin started");
	}

	stop() {
		DOM.removeStyle();
		console.log("Plugin stopped");
	}
}

BundleBD also supports CSS preprocessors like Sass and Less. Just import a file with the appropriate extension, and the bundler will automatically transpile it for you.

What Are CSS Modules?

A great feature included with the bundler is the ability to use CSS modules. You might already be familiar with them, but if not, here's a quick scenario:

Let's say two plugins inject styles, and both include the same class. This will result in conflicts between the two plugins, and the styling might not work as intended. You could change all the class names in the plugins to be unique, or you could use CSS modules instead.

When importing CSS modules, the bundler will take the normal stylesheet like this:

.class {
	color: red;
}

And turn it into something like this:

.Plugin-index-class {
	color: red;
}

This makes conflicts much less likely. CSS modules have many more use cases and features, but they won't be covered here. For more info see here.

Using CSS Modules

Using CSS modules is very similar to using regular stylesheets:

The bundler will treat any files with the extension .module.css, .module.scss, .module.sass, etc. as CSS modules.

The one difference is that the imported CSS module will export an object with the original classes as keys to get the processed classes. It will also export a css variable with a string of the processed CSS.

/* src/index.module.css */

.redText {
	color: red;
}
// src/index.jsx

import styleModule, { css } from "./index.module.css";
import { DOM } from "betterdiscord";

export default class MyAmazingPlugin {
	start() {
		DOM.addStyle(css);

		// Now the content of the element will be red!
		const element = <div className={styleModule.redText}>Hello World!</div>;
		console.log("Plugin started");
	}

	stop() {
		DOM.removeStyle();
		console.log("Plugin stopped");
	}
}

Importing Multiple Stylesheets

Often, it will be required to import multiple stylesheets, or import stylesheets in multiple files. BundleBD has a simple tool to deal with this: the styles module.

Importing from styles will give you a function that returns a string containing the processed contents of all of the imported stylesheets from every file in your plugin.

(Note: If you're only using the styles module to inject your stylesheets, you don't need to import anything from a stylesheet or import css from a CSS module, as seen in the example.)

// src/component.jsx

// There's no need to import 'css' here, since the contents will be included when importing from styles
import styleModule from "./component.module.css";

export default function MyComponent() {
	return <div className={styleModule.class}>Hello World</div>;
}
// src/index.jsx

import styles from "styles";
import { DOM } from "betterdiscord";
// There's no need to import a value here, since the contents will be included when importing from styles
import "./index.css";
import MyComponent from "./component.jsx";

export default class MyAmazingPlugin {
	start() {
		// 'styles' will return the contents of both index.css and component.module.css
		DOM.addStyle(styles());

		const element = <MyComponent />;
		console.log("Plugin started");
	}

	stop() {
		DOM.removeStyle();
		console.log("Plugin stopped");
	}
}

The bundler includes typings/autocomplete for the styles module, CSS modules, and stylesheets. If they are not being detected in Typescript, see here.

Other File Types

JSON Files

Using JSON files is pretty simple as well. Just import the file like normal, and you can access the object stored in it:

// src/strings.json

{
	"hello": "Hello World!",
	"goodbye": "Bye Bye!"
}
//src/index.js

import strings from "./strings.json";

export default class MyAmazingPlugin {
	start() {
		console.log(strings.hello); // Will log 'Hello World!'
	}

	stop() {
		console.log(strings.goodbye); // Will log 'Bye Bye!'
	}
}

To make sure you have type validation and autocomplete for JSON files in Typescript, see here, and confirm you have resolveJsonModule set to true.

Text Files

Plain old text files are also supported, and can be imported as strings:

src/message.txt:

Hello World!
//src/index.js

import message from "./message.txt";

export default class MyAmazingPlugin {
	start() {
		console.log(message); // Will log 'Hello World!'
	}

	stop() {}
}

Images

PNG, JPEG, and other image types are supported as well, and can be imported as Base64 data urls:

//src/index.jsx

import image from "./image.png";

export default class MyAmazingPlugin {
	start() {
		const image = <img src={image} />;
	}

	stop() {}
}

This behavior also applies within CSS files:

/* src/index.css */

.class {
	background-image: url(./image.png);
}

SVGs

SVGs are treated a little differently than normal images. Importing an SVG in a Javascript or Typescript file will, by default, also give you a Base64 data url:

//src/index.jsx

import url from "./icon.svg";

export default class MyAmazingPlugin {
	start() {
		const image = <img src={url} />;
	}

	stop() {}
}

However, you can also import a React Component to use with JSX by importing Component from the svg:

//src/index.jsx

import { Component as Icon } from "./icon.svg";

export default class MyAmazingPlugin {
	start() {
		const svg = <Icon width="18" height="18" />;
	}

	stop() {}
}