DEV Community

Cover image for How to Build a Bit Compiler for Svelte
Eden Ella
Eden Ella

Posted on • Updated on • Originally published at blog.bitsrc.io

How to Build a Bit Compiler for Svelte

Written by Giancarlo Buomprisco. Published originally on "Bits and Pieces".

How to extend Bit to share components built with your frameworks and tools of choice

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.

Example: Bit’s component hub and playgroundExample: Bit’s component hub and playground

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.

A look at Svelte’s API

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.

Wiring up the Svelte compiler 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:

  • handle gracefully when the list of files is empty

  • handle gracefully when the dist path provided by the user doesn’t exist. Of course, the following implementation is naive, as it will simply check if the path exists or not, and will create the path if it doesn’t

    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:

  • we loop through the files and compile them one by one using svelte.compile

  • we retrieve the component’s filename and we write the resulting code to the dist path received from the configuration

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

Implementing Bit’s Compiler API

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();

Using Our Compiler with Bit

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:

  • one with a collection of components you want to export

  • one with an application that needs to use the exported components

Let’s configure our project so that our configuration will point to the compiler we created.

Importing the Compiler

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

Configuring the Project with the custom 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.

Exporting Components

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>

Using exported components in your Svelte app

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!

I hope you enjoyed this article! If you did, follow me on* Medium or my website for more articles about Software Development, Front End, RxJS, Typescript and more!

Learn More

Top comments (2)

Collapse
 
pcrunn profile image
Alexander P.

Pretty sure you meant how instead of hot

Collapse
 
giteden profile image
Eden Ella

Fixed. Thanks!