Skip to content

Getting Started

Neodymium edited this page Jul 30, 2022 · 11 revisions

Installation

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

npm i bundlebd -D

Basics

First make sure you read the BetterDiscord Plugin Docs (Coming Soon) and know at least the basics of making plugins.

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");
	}
}

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.

Basic Configuration

Now that you have your bundled plugin, you might want to configure it beyond the defaults. The default name 'Plugin' doesn't sound very appealing or descriptive.

Luckily, customizing the plugin's name is easy.

To start, create a config.json file in the same folder as your plugin's main file. So in our example, the file would be in src. In this configuration file, you can add information to the plugin's meta, like it's name:

// src/config.json

{
	"meta": {
		"name": "MyAmazingPlugin"
	}
}

The name in the meta will also be stripped of any whitespace and used in the bundled plugin file's name.

In the config.json file, you can specify more information about your plugin as well. For example, the contents of the file might look like:

// src/config.json

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

Now when you bundle your plugin, the bundler will detect the configuration file and use its information instead of the defaults. For all configuration options, see Plugin Configuration.

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 ES 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, Zlibrary, and BundleBD's Built-in Modules, 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 may result in some errors.

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 the project's root directory with the following contents:

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

See Typescript Configuration for more recommended TSConfig options.

The meta Object

Eventually, BetterDiscord will pass a plugin's meta into the plugin or its constructor for it to access its own metadata. Since this is not implemented yet, every plugin bundled with BundleBD can globally acces a meta object, allowing the plugin to access its own metadata. For all of the keys available on the meta object, see here.

// src/config.json

{
	"meta": {
		"name": "MyAmazingPlugin",
		"author": "Neodymium",
		"description": "A plugin that does absolutely nothing",
		"version": "1.0.0"
	}
}
// src/index.js

export default class MyAmazingPlugin {
	start() {
		console.log(meta.name); // "MyAmazingPlugin"
		console.log(meta.author); // "Neodymium"
		console.log(meta.description); // "A plugin that does absolutely nothing"
		console.log(meta.version); // "1.0.0"
	}

	stop() {}
}

The bundler includes typings/autocomplete for the meta object. If they are not being detected, see here.

Using BdApi

Utilizing BdApi is just as easy as it is in a normal BetterDiscord plugin, since BdApi is still globally available:

// src/index.js

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

	stop() {}
}

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

Using Stylesheets

(Currently BundleBD supports CSS, SCSS, and Sass, but more CSS extension languages may be added in the future)

As should be expected by this point, stylesheets are also easy to use, but their usage requires a few more steps. The first step is to simply import the stylesheet you want to use in your plugin. For example:

// src/index.js

import "./index.css";

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

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

Injecting Styles

The second step is to inject any imported stylesheets into the DOM when the plugin is started. When you import a stylesheet, it will be automatically loaded into the bundler's built-in Styles class, which is bundled with your plugin by default whenever you import a stylesheet. In order to inject the styles, you need to use Styles.inject(). Here's an example of injection in action:

// src/index.js

// When the bundler sees a stylesheet is imported, it will automatically load it into the Styles class.
import "./index.css";
import { Styles } from "bundlebd";

export default class MyAmazingPlugin {
	start() {
		// Then, when the plugin is started, you can use inject() to inject the styles into Discord.
		Styles.inject();
		console.log("Plugin started");
	}

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

There's only one more step left, but it's crucial: cleanup. Just like you use Styles.inject() when your plugin starts, you need to use Styles.clear() when your plugin stops to remove all injected styles from Discord. Here's an example:

// src/index.js

import "./index.css";
import { Styles } from "bundlebd";

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

	stop() {
		// Always remember to clear the styles whenever you inject!
		Styles.clear();
		console.log("Plugin stopped");
	}
}

Imports and URLs

Local @imports and URLs included in imported stylesheets will be bundled with the plugin. For more information on how imports and URLs are handled, see here, and for information on how local files like images will be bundled, see Other File Types.

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 with local class like this:

/* A local class */
.class {
	color: red;
}

And turn it into something like this with global classes:

/* A global class that will be used in the injected stylesheet */
.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 stylsheets:

The bundler will treat any files with the extention .module.css, .module.scss, .module.sass, etc. as CSS modules. Also, if you include a query string of ?module to any normal stylesheet, it will be treated as a CSS module.

They are automatically loaded just like normal stylesheets, and injected and cleared in the same way. The one difference is that instead of using hardcoded strings as class names, the CSS module will export an object with the local class names as keys to get the global class names. Here's an example to make the whole process easier to see:

/* src/index.module.css */

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

import styleModule from "./index.module.css";
import { Styles } from "bundlebd";

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

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

Or with the query string option (my personal preference):

/* src/index.css */

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

import styleModule from "./index.css?module";
import { Styles } from "bundlebd";

//... Rest of the Code ...

Advanced Options

The bundler has more options for using stylesheets, but they will likely be used much less than basic stylesheet and CSS module usage.

If for whatever reason you don't want an imported stylesheet or CSS module to be automatically loaded for injection, or if you don't want the built-in Styles class to be bundled with your plugin, you can add /* ignoreLoad */ to the top of the stylesheet or CSS module. Since it will not be loaded, calling Styles.inject() will not inject the stylesheet, and you'll need to do whatever you want to with it manually.

/* ignoreLoad */

/* this stylesheet won't be loaded for injection automatically */
.class {
	color: red;
}

Whether loaded or not, a stylesheet will export a string containing the contents of the stylesheet with imports and urls resolved. A CSS module will also export a string of the module's contents (after being processed to its global form) that can be accessed using the _content property.

/* src/index.css */

.class {
	color: red;
}
// src/index.js

import stylesheet from "./index.css";
import styleModule from "./index.css?module";

export default class MyAmazingPlugin {
	start() {
		console.log(stylesheet); // will log '.class { color: red; }'
		console.log(styleModule._content); // will log '.MyAmazingPlugin-index-class { color: red; }'
	}

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

Using both of these extra features, you can use your own implementation of injecting styles, or achieve more advanced behavior like injecting different stylesheets dynamically or at different times.

// src/index.js

import stylesheet from "./index.css";
import otherStyleModule from "./styles.css?module";

export default class MyAmazingPlugin {
	start() {
		setTimeout(() => {
			this.styles = stylesheet;
			BdApi.injectCSS("MyAmazingPlugin", this.styles);
		}, 1000);

		setTimeout(() => {
			this.styles += otherStyleModule._content;
			BdApi.injectCSS("MyAmazingPlugin", this.styles);
		}, 3000);
	}

	stop() {
		BdApi.clearCSS("MyAmazingPlugin");
	}
}

Built-in Styles Module

BundleBD includes a built-in Styles module that provides utilities for injecting and clearing styles. It is not required, but reccommended to use it, especially if imported styleheets are being automatically loaded by the Styles module. For more information, see Styles.

The bundler can easily bundle plugins that use Zlibrary. All you need to do is set zlibrary to true in your plugin's config.json file:

// src/config.json

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

Usage

Using the library in your plugin itself is very simple as well, just import the library from @zlibrary and the base Plugin class from @zlibrary/plugin and use them as you would normally.

// src/index.js

import Plugin from "@zlibrary/plugin";
import { DiscordModules } from "@zlibrary";

export default class MyAmazingPlugin extends Plugin {
	onStart() {
		const UserStore = DiscordModules.UserStore;
		console.log(UserStore.getCurrentUser());
	}

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

Warnings

When bundling, BundleBD will warn you if you import the library without setting the config option to true, or if you use the library without extending your plugin from the base Plugin class, both of which will prevent your plugin from working properly.

Typings

BundleBD includes typings/autocomplete for ZLibrary. If they are not being detected, 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, 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, JPG, and JPEG image are supported as well, and can be imported as Base64 encoded urls:

//src/index.jsx

import image from "./image.png";

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

	stop() {}
}

This behavior also appies to 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 Javascrript or Typescript file will, by default, give you a React Component to use with JSX:

//src/index.jsx

import Icon from "./icon.svg";

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

	stop() {}
}

If you instead want to import it as a Base64 string, you can use the ?url resource query:

//src/index.jsx

import url from "./icon.svg?url";

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

	stop() {}
}

In CSS files, SVGs will be treated the same as normal images.