DEV Community

Daniel Santos
Daniel Santos

Posted on • Updated on

The astro-deck's CLI

Last month I wrote about the beginning of astro-deck, a couple things happened since then, and today I want to talk about its CLI.

I didn't know right from the beginning how to distribute astro-deck, I was just building some functionalities on top of Astro, and hoping to someday find out a better way to organize it.

At its early days, astro-deck was nothing more than scripts inside package.json that just pointed to astro CLI commands:

"scripts": {
    "deck": "tsx src/scripts/deck.ts",
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
}
Enter fullscreen mode Exit fullscreen mode

Everything was working fine, but I was testing the wrong way, I was creating a deck file inside the project and running these npm script. This is not the way users interact with tools like astro, nextjs and etc, they interact directly with a CLI.

drawing of a stick figure, pointing to the astro's CLI, in the image, the stick figure asks: "What is astro?"

yeah, that was literally me remembering that I interact with CLIs everyday

I read a little about CLIs, it really is a fascinating world, here are some links that helped me a lot:

I ended up choosing NodeJS + yargs to build the CLI, there is a lot of content on the internet about them, and the short time I used it, it seemed like a good combo.

let's get to work

I started with the simplest, the deck command, as you saw in the first snippet, its only mission was to call a function inside src/scripts, I changed this script location and made some adjustments, here's the final version of the deck command:

const command: CommandModule<{}, { path: string }> = {
    command: "deck <path>",
    describe: "Build a presentation from a given .mdx file",
    builder: {
        path: {
            describe: "Path to the .mdx file",
        },
    },

    handler: function (args) {
        const astroDeckPagesFolder = resolve(
            __dirname,
            "..",
            "..",
            "..",
            "src",
            "pages",
        );

        try {
            const filePath = resolve(args.path);
            mdxToPresentation(filePath, astroDeckPagesFolder);
        } catch (error) {
            console.error(`[astro-deck] error: ${(error as Error).message}`);
        }
    },
};
Enter fullscreen mode Exit fullscreen mode

It receives a .mdx file, divides it into several other files, and saves everything in the pages folder.

The next one was the dev command, all it needs to do is to start Astro development server. As I'm writing this, Astro does not provide a way to start it programatically, so the only option is to start a new process the old way. NodeJS has child_process to make this possible, but I choose execa since it provides a better API.

My first attempt looked like this:

await execa("astro", [astroOptions, ...astroCommands.map(toString)], {
    stdio: "inherit"
});
Enter fullscreen mode Exit fullscreen mode

Everything sounds ok, but it doesn't work!

A white board with a joke about how the previous code snippet didn't work

This problem happens because the user does not have Astro installed globally/as a dependency.

I also tried npm explore as you can see here.

It worked but I wanted to know if there's a better way to do this, to my surprise, execa has an awesome option called preferLocal, if set to true, it will look first for locally installed binaries.

await execa("astro", [astroOptions, ...astroCommands.map(toString)], {
    stdio: "inherit",
    cwd: astroDeckFolder,
    preferLocal: true,
});
Enter fullscreen mode Exit fullscreen mode

And then:

A white board with a joke about the previous code snippet

This is the final version of the astro helper:

import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execa } from "execa";
import type { ArgumentsCamelCase } from "yargs";

export type AstroOptions = "dev" | "build" | "preview";

const __dirname = dirname(fileURLToPath(import.meta.url));

/**
 * Runs the Astro CLI with the given options and commands
 */
export async function astro(
    args: ArgumentsCamelCase,
    astroOptions: AstroOptions,
) {
    const astroCommands = args._.slice(1);

    const astroDeckFolder = resolve(__dirname, "..", "..");

    await execa("astro", [astroOptions, ...astroCommands.map(toString)], {
        stdio: "inherit",
        cwd: astroDeckFolder,
        preferLocal: true,
    });
}
Enter fullscreen mode Exit fullscreen mode

Once the development server starts, it will run the deck command under the hood and listen to any changes inside the /pages folder, thanks to the deckWatcher helper, the dev command will also listen to changes in the presentation file, and run the deck command again, if needed.

There isn't too much to talk about build and preview commands, they just move folders around and call Astro.

wrapping up

The CLI will be the bridge between the users and the functionalities of astro-deck, for now, I think it is well organized and can do the job, but remember, this is a open-source project, so suggestions and contributions are always welcome, feel free to open a issue or to tag me at Twitter.

The next steps of astro-deck will be related to improve the support of default components and themes, they will be hard tasks, and I hope to share more about them as soon as possible.

Top comments (0)