Astro/MDX Directory Tree Component

Tue Nov 07 2023

Tree branches in a fractal pattern

I got tired of copying and pasting weird ASCII characters in order to write out directory trees so I wrote an Astro component to convert YAML into a format that looks like the output of the Linux tree command!

I’ll show you how to get from this:

project:
  - config:
    - __init__.py
    - asgi.py
    - settings.py
    - urls.py
    - wsgi.py
  - manage.py
  - Pipfile
  - Pipfile.lock

to this:

{
  "project": [
    {
      "config": [
        "__init__.py",
        "asgi.py",
        "settings.py",
        "urls.py",
        "wsgi.py"
      ]
    },
    "manage.py",
    "Pipfile",
    "Pipfile.lock"
  ]
}

to this:

└── project/
    ├── config/
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── manage.py
    ├── Pipfile
    └── Pipfile.lock

This is the result of running the command django-admin startproject config . inside a directory called project.

I won’t go into too much detail about how the code for this works. There are plenty of great resources on how to pretty print directory trees.

All I really did was rewrite the general function outline in TypeScript and adapt it to take in an object as input instead of a filepath.

Step One: Convert YAML String to JavaScript Object

I used the JavaScript package js-yaml to convert a YAML-formatted string to an object. Unfortunately, the load function from this package returns an object of type unknown so I had to define and use my own types.

import { load as loadYaml } from "js-yaml";

// union of all possible *individual* node types
type SingleNode = string | number | boolean | undefined | TreeNode;

// recursive type for tree of nodes
interface TreeNode {
  [key: string]: SingleNode[] | null;
}

const yamlString = `
root:
  - sub1:
    - file.txt
  - sub2:
    - file.txt
`;

const yamlObj = loadYaml(yamlString) as TreeNode;

Step Two: Use Object to Create Tree Representation

This isn’t meant to be an introduction to recursive functions, so I won’t walk through each line of the code. Hopefully the included comments are enough for folks newer to recursion to follow along.

const createTree = (yamlObj: TreeNode): string => {
  // "header" strings
  const PIPE = "│   ";
  const BLANK = "    ";

  // node "prefix" strings
  const ELBOW = "└── ";
  const TEE = "├── ";

  const res: string[] = [];

  // inner recursive function to build tree strings
  const traverseTree = (node: SingleNode, header = "", last = true): void => {
    // is the node an object?
    if (typeof node === "object") {
      // all object nodes should have a single key
      for (const key in node) {
        res.push(`${header}${last ? ELBOW : TEE}${key}/`);

        const children = node[key]; // SingleNode[] | null
        // check that children !== null
        if (children) {
          for (let i = 0; i < children.length; i++) {
            const child = children[i];
            // build up header string
            const h = header + (last ? BLANK : PIPE);
            // is this node the last child of a parent?
            const l = i === children.length - 1;
            traverseTree(child, h, l);
          }
        }
      }
    /**
     * else if node is (a):
     * - string
     * - number
     * - boolean
     * - undefined
     */
    } else {
      res.push(`${header}${last ? ELBOW : TEE}${node}`);
    }
  }

  traverseTree(yamlObj);

  return res.join("\n");
};

This might save me some time in the long run, but honestly it was just fun to build. If you or a loved one ever wants to display a directory tree, consider using a free tool someone else already built!


View the MDX for this page or submit an issue if you noticed any errors!