Custom component properties

Learn using the CLI hooks to add custom component properties to your scan data.

Omlet's CLI hooks allow you to run custom scripts on Omlet CLI scans before the final result is sent to the Omlet backend. This offers an interface to process the component data and add custom properties.

Once those custom properties are added to components, you and your teammates can create charts based on them. Here are some use cases that you can achieve by utilizing the CLI hooks to create tags with custom properties:

  • Automatically tag components based on their category, such as Icons or Pages

  • Identify the product team or user who created a component

  • Find components that have missing documentation or tests

  • Differentiate visual vs. non-visual components

Custom properties

The custom properties can be string, number, date or boolean values. For example, you can add additional information such as:

  • owner: The code owner information in the CODEOWNERS file or any custom source. i.e. @acme/design-system, @acme/backoffice, @acme/marketplace

  • hasStories: True or false depending on whether Storybook stories exist for a given component

  • hasTests: True or false depending on test specs that exist for a given component

Components can be tagged accordingly by applying the following filters:

  • team-ds: metadata.owner equals "@acme/design-system"

  • team-marketplace: metadata.owner equals "@acme/marketplace"

  • storybook: metadata.hasStories equals true

  • has-test: metadata.hasTests equals true

  • no-tests: metadata.hasTests equals false

Scanning with a hook script

You can place your hook script in a preferred directory/folder. Once your script is ready, you scan your repository using the --hook-script command line argument.

npx @omlet/cli analyze --hook-script ./path/to/hook-script.js

afterScan hook

Currently, afterScan is the only hook supported by Omlet CLI. This hook is executed after a scan is completed successfully.

hook-script.js
/**
 * @type {import('@omlet/cli').CliHookModule}
 */
module.exports = {
    async afterScan(components) {
        for (const component of components) {
            component.setMetadata("hasStories", await hasStories(component.filePath));
            component.setMetadata("hasTests", await hasTests(component.filePath));
        }
    }
}

If you have the @omlet/cli package installed as a dependency, the @type annotation will enable auto-complete features and inline documentation for functions and components.

Here are the properties of the Component object provided with the afterScan hook:

Property
Type
Description

id

String

Unique identifier for the component.

name

String

Name of the component as exported in the source code.

createdAt

Date

Creation date of the component, extracted from the git history. Optional.

updatedAt

Date

Last updated date of the component, extracted from the git history. Optional.

packageName

String

Package name the component belongs to.

filePath

String

File path to the component within the repository.

props

Array of objects

[{
    name: String,
    defaultValue: String
}, ...]

List of props of the component, including name and optional default value.

htmlElementsUsed

String[]

List of HTML elements used within the component.

children

Component[]

Child components detected in the scan.

parents

Component[]

Parent components detected in the scan.

Here are a few things to know about the afterScan hook:

  • afterScan hook can be defined both as an async or sync function.

  • The data passed to the hook is read-only, except the metadata that is editable with the setMetadata function.

  • You can use npm packages as long as they're available in the Node.js runtime.

Analyzing custom components

Once you have your custom properties on Omlet, you can create tags based on them to analyze the component usage. To learn more, check out Component tags and take a look at Create custom charts documentations.

If you have string-based custom properties, such as Team or Codeowner, you can directly analyze them in the charts—no need to create individual tags for their values.

Sample hook scripts

Code owners

You can add codeowners package as a dependency to your repository and use the following code example to add the code owner information to each component's metadata.

hook-script.js
const Codeowners = require("codeowners");

const repo = new Codeowners();
function getOwners(filePath) {
    const owners = repo.getOwner(`${filePath}`);
    if (owners.length === 0) {
        owners.push("unknown");
    }
    return owners
        .filter((o) => o !== "")
        .map((o) => `${o}`)
        .join(",");
}

/**
 * @type {import('@omlet/cli').CliHookModule}
 */
module.exports = {
    async afterScan(components) {
        for (const component of components) {
            const owners = getOwners(component.filePath);
            component.setMetadata("owners", owners);
        }
    },
};

Package version

The following hook function adds the design system package version to each component's metadata.

hook-script.js
const fs = require("fs/promises");
const path = require("path");
const pkgUp = require("pkg-up");

/**
 * Find the closest package.json file and retrieve the design system version.
 *
 * @param {string} filePath - The file path to start the search from.
 * @returns {Promise<string>} - The design system version or 'unknown' if not found.
 */
async function findDSPackageVersion(filePath) {
  try {
    const packageJsonPath = await pkgUp({ cwd: filePath });
    if (!packageJsonPath) return "unknown";

    const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
    const dependencies = packageJson.dependencies || {};
    const devDependencies = packageJson.devDependencies || {};

    // Example: Replace '@design-system' with your actual design system package name.
    return dependencies["@design-system"] || devDependencies["@design-system"] || "unknown";
  } catch (error) {
    console.error(`Error finding DS version for ${filePath}:`, error);
    return "unknown";
  }
}

/**
 * @type {import('@omlet/cli').CliHookModule}
 */
module.exports = {
  async afterScan(components) {
    const promises = components.map(async (component) => {
      const dsVersion = await findDSPackageVersion(component.filePath);
      component.setMetadata("DS Version", dsVersion);
    });

    // Wait for all metadata to be set
    await Promise.all(promises);
  },
};

Test and Storybook coverage

The following hook function add hasTests and hasStories properties to each component's metadata to mark the components in terms of their test and Storybook coverages.

hook-script.js
const { promises: fs, constants: fsConstants } = require("fs");
const path = require("path");

const fileLookupCache = new Map();
async function exists(filePath) {
    const absPath = path.resolve(__dirname, filePath);
    if (fileLookupCache.has(absPath)) {
        return fileLookupCache.get(absPath);
    }
    try {
        await fs.access(absPath, fsConstants.F_OK);
        fileLookupCache.set(absPath, true);
        return true;
    } catch {
        fileLookupCache.set(absPath, false);
        return false;
    }
}

function hasTests(filePath) {
    const testFilePath = filePath.replace(/(.)(\.[jt]sx?)$/, "$1.test$2");
    return exists(testFilePath);
}

function hasStories(filePath) {
    const testFilePath = filePath.replace(/(.)(\.[jt]sx?)$/, "$1.stories$2");
    return exists(testFilePath);
}

/**
 * @type {import('@omlet/cli').CliHookModule}
 */
module.exports = {
    async afterScan(components) {
        for (const component of components) {
            component.setMetadata("hasStories", await hasStories(component.filePath));
            component.setMetadata("hasTests", await hasTests(component.filePath));
        }
    }
}

Visual components

The following hook function marks components as visual if they render a visual HTML element (e.g. <div>, <img />) or another component marked as visual. Here you can find this snippet with a full list of html tags.

hook-script.js
// A set of HTML tags that are considered to be UI elements
const htmlUiTags = new Set([
    "a",
    "abbr",
    "acronym",
    "address",
    // ...
]);
/**
 * @type {import('@omlet/cli').CliHookModule}
 */
module.exports = {
    afterScan(components) {
        const visualComponents = new Set();
        let updated;
        do {
            updated = false;
            for (const component of components) {
                if (visualComponents.has(component.id)) {
                    continue;
                } else if (component.htmlElementsUsed.some((tag) => htmlUiTags.has(tag))) {
                    component.setMetadata("isVisualComponent", true);
                    visualComponents.add(component.id);
                    updated = true;
                } else if (component.children.some(child => visualComponents.has(child.id))) {
                    component.setMetadata("isVisualComponent", true);
                    visualComponents.add(component.id);
                    updated = true;
                } else {
                    component.setMetadata("isVisualComponent", false);
                }
            }
        } while (updated);
    }
}

Deprecated components

The following hook function marks components that contains the @deprecated comment.

const { promises: fs, constants: fsConstants } = require("fs");
const path = require("path");


// Function to check if a file contains the @deprecated comment
async function isDeprecated(filePath) {
    try {
        const fileContent = await fs.readFile(filePath, "utf-8");
        const deprecatedPattern = /@deprecated/; // Simple pattern to detect @deprecated
        return deprecatedPattern.test(fileContent);
    } catch (err) {
        console.error(`Error reading file ${filePath}:`, err);
        return false;
    }
}

/**
 * @type {import('@omlet/cli').CliHookModule}
 */
module.exports = {
    async afterScan(components) {
        for (const component of components) {
             // Check if component is deprecated
             const deprecated = await isDeprecated(component.filePath);
             component.setMetadata("Is deprecated", deprecated);
        }
    }
}

Last updated