Bit is a tool that helps teams build components, test and render them in isolation, search and visualize them in a neat UI, and share them with other teams with ease.
While Bit provides official tooling for the most important frameworks out there, you also have the possibility to extend and use it for a new shiny framework or for your own tools, or if you have a configuration that requires it.
While going through the documentation I noticed that Bit has an extensive configurability that allows you to customize their CLI platform with new functionalities: one of them, writing a custom compiler, immediately caught my attention, so I set out to build an extension for one of the hottest libraries to build components: Svelte.
In this article, I am going to show you how to build a rudimental compiler for Svelte and how you can apply the same approach to any type of tooling.
Notice: the following is a simple, native, quick solution and does not aim to match the quality of Bit’s official compilers. But it does aim to show you how quick and easy it is to build your own compilers.
The first thing I did to make sure I could build a compiler for Svelte components was to analyze the API provided: as expected, it was way easier than with other major frameworks. Svelte’s API is small, comprehensive and works amazingly.
The following snippet is taken directly from svelte.dev:
const svelte = require('svelte/compiler');
const result = svelte.compile(source, {
// options
});
That’s pretty much it! Now we need to find out how to wire this up with Bit’s API.
To understand well how to build a Bit compiler, I went through their list of compilers officially available.
The first thing to do is to start a new Typescript project called svelte-compiler
and initiate a new Bit project with the following command:
▶ bit init
If you’re not logged in, also run:
▶ bit login
As the documentation states, the interface our compile function needs to implement is pretty simple:
function compile(files: Vinyl[], distPath: string): Promise<Vinyl[]> {
// code here...
return compiledFilesArray;
}
Vinyl is a wrapper class to handle files
Let’s see how we can implement this function step by step. Initially, we are concerned about two things:
export function compile(
files: Vinyl[],
distPath: string
): Promise<Vinyl[]> {
if (files.length === 0) {
return Promise.resolve([]);
}
if (!fs.existsSync(distPath)) {
console.warn('Path does not exist. Automatically generating path');
fs.mkdirSync(distPath);
}
// more code
}
The next step is to read the Svelte files, compile them, and then write them to the dist folder provided by the configuration. Let’s create a standalone function for that:
svelte.compile
function compileSvelteFiles(
files: Vinyl[],
distPath: string
) {
files.forEach(file => {
const source = fs.readFileSync(file.path, 'utf-8');
const result = svelte.compile(source, {});
const fileNamePathParts = file.path.split('/');
const fileName = fileNamePathParts[
fileNamePathParts.length - 1
];
const fileDistPath = path.resolve(distPath + '/' + fileName);
fs.writeFileSync(fileDistPath, result.js.code);
});
}
As we’ve seen initially, the function requires us to return a list of Vinyl
files. What we will do next is to loop through the files created in the previous step and push them to an array as Vinyl
files:
function getVinylFiles(distPath: string): Vinyl[] {
const outputFiles = [];
fs.readdirSync(distPath).forEach(name => {
const fileDistPath = distPath + '/' + name;
const fileContent = fs.readFileSync(fileDistPath);
outputFiles.push(
new Vinyl({
contents: fileContent,
base: distPath,
path: path.join(distPath, name),
basename: replaceSvelteExtension(name)
})
);
});
return outputFiles;
}
Finally, we wrap the result of the previous function in a Promise:
export function compile(
files: Vinyl[],
distPath: string
): Promise<Vinyl[]> {
if (files.length === 0) {
return Promise.resolve([]);
}
if (!fs.existsSync(distPath)) {
console.warn('Path does not exist. Automatically generating path');
fs.mkdirSync(distPath);
}
return new Promise(resolve => {
compileSvelteFiles(files, distPath);
resolve(getVinylFiles(distPath));
});
}
First, we want to install some dependencies needed to use Bit’s compiler
▶ npm i @bit/bit.envs.common.compiler-types
Now, we can run our compile
function and wire it up with Bit’s compiler API. Bit provides an interface Compiler
we can implement:
import {
Compiler,
InitAPI,
CompilerContext,
ActionReturnType
} from '@bit/bit.envs.common.compiler-types';
import { compile } from './compile';
export class SvelteCompiler implements Compiler {
init(ctx: { api: InitAPI }) {
return {
write: true
};
}
getDynamicPackageDependencies(
ctx: CompilerContext, name?: string)
{
return {};
}
async action(ctx: CompilerContext): Promise<ActionReturnType> {
const dists = await compile(ctx.files, ctx.context.rootDistDir);
return { dists };
}
}
Finally, we export our compiler with a barrel file index.ts
:
import { SvelteCompiler } from './svelte-compiler';
export { SvelteCompiler };
export default new SvelteCompiler();
Now that our compiler is finished, we may want to export it to Bit or we can run it locally by pointing our configuration to the compiled file.
In order to export it to Bit, you can run the following commands:
▶ bit add .
▶ bit tag --all 1.0.0
▶ bit export <collection_name>
Assuming, you already have 2 repositories:
Let’s configure our project so that our configuration will point to the compiler we created.
Run the following command in your Bit project so that we can use the compiler for the components project:
▶ bit import <your-bit-compiler> --compiler
Open your package.json and set the following values:
"bit": {
"env": {
"compiler": "<your-bit-compiler>@<version>"
},
"componentsDefaultDirectory": "components/{name}",
"packageManager": "npm"
}
Of course, make sure you set the actual name and versions of your compiler.
Now that the configuration is set, it’s time to export our components. Let’s assume our components live in the folder src/components
and that we have 1 component called Alert.svelte
.
We start by tracking the components, e.g. we tell Bit where our components live:
▶ bit add src/components/*
tracking component alert:
added src/components/Alert.svelte
We can then proceed and build the components with the following command:
▶ bit build
We tag the components with a version:
▶ bit tag --all 1.0.0
Finally, we export and sync them with Bit:
▶ bit export <your-collection-name>
exported 1 components to scope <your-collection-name>
Once we exported our components from our component library, it’s time to use them in our application project.
In order to retrieve them, we use Bit’s import command. We assume we only want to import the Alert.svelte
component. Assuming we have already initialized Bit in the project, we run the following command:
▶ bit import <your-collection-name>/alert
successfully ran npm install at <path-to-project>/components/alert
successfully imported one component
- up to date <your-collection-name>/alert
Finally, let’s update your application to use our newly imported component:
<script>
import Alert from "../components/alert";
</script>
<Alert type="danger">My Alert</Alert>
And that’s all!
We can now build components with Svelte, export them to our Bit collection and use them within our project.Of course, this example is naive and far from what an officially supported compiler would look like, but it can help you think what compilers can be implemented and integrated with Bit in only a few lines of code.
If you need any clarifications, or if you think something is unclear or wrong, do please leave a comment! Thank you for reading !
#javascript #typescript #web-development