npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

arch-unit-js

v1.2.0

Published

ArchUnit clone for node.js projects

Readme

:page_facing_up: About

A JavaScript/TypeScript library for enforcing architectural rules and constraints in your codebase. Inspired by ArchUnit for Java, this tool provides a fluent API to define and validate architectural boundaries, naming conventions, and dependency rules. It is agnostic about the testing framework & OS systems ! Also provides support for both ESModules and CommonJS projects !

Note: Backend-focused (frontend support coming soon).

:hammer_and_wrench: Supported OS

  • [x] Mac OS
  • [x] Linux
  • [x] Windows

:ledger: Features

  • Dependency Rules: Control which modules can depend on others (dependsOn, onlyDependsOn)
  • Naming Conventions: Enforce consistent file naming patterns (haveName, onlyHaveName)
  • Code Metrics: Validate lines of code thresholds (haveLocLessThan, haveLocGreaterThan)
  • Project Metrics: Validate code project percentage thresholds (haveTotalProjectCodeLessThan, haveTotalProjectCodeLessOrEqualThan)
  • Cycle Detection: Prevent circular dependencies (shouldNot.haveCycles)
  • Fluent API: Intuitive, readable syntax for defining architectural rules

:racing_car: Getting Started

Installation

Install using npm

npm install --save-dev arch-unit-js

JavaScript - (Basic Scenario)

Let's get started by writing a simple function that generates a UUID using the lib uuid. First, create a uuid.js file, inside a utils directory:

// file path: ./utils/uuid.js
const { v4 as uuidv4 } = require('uuid');

export function generateUUID() {
  return uuidv4();
}

Then create a test file utils-arch.spec.js in a tests directory, where we are going to test that all files inside the utils directory should have the uuid lib inside:

// file path: ./tests/utils-arch.spec.js
const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'], // Positive Glob pattern, where you specify all extension types your application has
  includeMatcher: ['<rootDir>/**'], // Positive Glob pattern, where you specify all files and directories based on the project <rootDir>
  ignoreMatcher: ['!**/node_modules/**'], // (Optional) - Negative Glob pattern, where you specify all files and directories you do NOT want to check
};

// We are using Jest, but you can use any other testing library
describe('Architecture Test', () => {
  it('"./utils/uuid.js" file should depend on "uuid" lib', async () => {
    await app(options).projectFiles().inDirectory('**/utils/**').should().dependsOn('uuid').check(); // No need to expect, if the dependency is not found it throws an error
  });
});

Now run the test and congrats 🥳, you just tested your application topology !

module-alias

arch-unit-js also provides support for applications which still use [email protected] !

Create a file register.js , in the root of your project, function which calls the module-alias first:

// file path: ./register.js
'use strict';

const path = require('path');
const moduleAlias = require('module-alias');
const baseDir = __dirname;

moduleAlias.addAliases({
  '#domain': path.join(baseDir, 'domain'),
  '#usecases': path.join(baseDir, 'use-cases'),
});

Now let's create a simple domain layer in a directory with a file user.js:

// file path: ./domain/user.js
export class User {
  constructor(id, name) {}
}

To use the domain layer, create the use-cases layer within a directory with the same name, with a file called create-user.js:

// file path: ./use-cases/create-user.js
const { User } = require('#domain/user');

const createUserUseCase = () => new User(1, 'Roko');

Now, let's test if create-user.js does depends on the #domain layer, create a tests directory and inside create a arch-use-case.test.js file.

// file path: ./tests/arch-use-case.test.js
const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'], // Positive Glob pattern, where you specify all extension types your application has
  includeMatcher: ['<rootDir>/**'], // Positive Glob pattern, where you specify all files and directories based on the project <rootDir>
  ignoreMatcher: ['!**/node_modules/**'], // (Optional) - Negative Glob pattern, where you specify all files and directories you do NOT want to check
};

// We are using Jest, but you can use any other testing library
describe('Architecture Test', () => {
  beforeAll(() => {
    require('../register'); // calls the `module-alias` and stores the alias in the node Modules package
  });

  it('"./createUserUseCase.js" file should depend on "#domain"', async () => {
    await app(options)
      .projectFiles()
      .inFile('**/usecases/create-user.js')
      .should()
      .dependsOn('**/domain/**')
      .check(); // No need to expect, if the dependency is not found it throws an error
  });
});

And there you have it congrats again 🥳 , you successfully tested your project dependencies which uses module-alias !

webpack

arch-unit-js also provides support for applications which use [email protected] !

In this section we are going to explore some scenarios using webpack. In the first example let's use a single build webpack.config.js file given in the example below.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// single - webpack.config.js
module.exports = {
  entry: './main/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true,
  },
  resolve: {
    extensions: ['.js'],
    alias: {
      '@domain': path.resolve(__dirname, 'domain'),
      '@use-cases': path.resolve(__dirname, 'use-cases'),
      '@infra': path.resolve(__dirname, 'infra'),
      '@main': path.resolve(__dirname, 'main'),
    },
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ],
  },
  plugins: [new HtmlWebpackPlugin({ template: './index.html' })],
  devServer: {
    static: path.resolve(__dirname, 'public'),
    port: 5173,
    historyApiFallback: true,
  },
};

In this example we wanna test if the files within the directory **/use-cases/** are using the files within **/domain/** to assert usage according to the 'clean architecture' standards. Since webpack is being used, we need to use let explicit within the path(options) using the following !

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'], // Positive Glob pattern, where you specify all extension types your application has
  includeMatcher: ['<rootDir>/**'], // Positive Glob pattern, where you specify all files and directories based on the project <rootDir>
  ignoreMatcher: ['!**/node_modules/**'], // (Optional) - Negative Glob pattern, where you specify all files and directories you do NOT want to check
  webpack: {
    path: '<rootDir>/webpack.config.js', // Path to project 'webpack.config.js' - (using <rootDir> as wildcard)
  },
};

// We are using Jest, but you can use any other testing library
describe('Architecture Test', () => {
  it('"**/use-cases/**" files should depends on "@domain"', async () => {
    await app(options)
      .projectFiles()
      .inDirectory('**/usecases/**')
      .should()
      .dependsOn('**/domain/**')
      .check(); // No need to expect, if the dependency is not found it throws an error
  });
});

See, it is pretty easy !

In the next example let's explore a webpack.config.js file which has muiltiple builds down below.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

// single - webpack.config.js
module.exports = [
  {
    name: 'client'
    entry: './main/client.js',
    output: {
      path: path.resolve(__dirname, 'dist', 'client'),
      filename: 'bundle.client.js',
      clean: true,
    },
    resolve: {
      extensions: ['.js'],
      alias: {
        '@domain': path.resolve(__dirname, 'domain'),
        '@use-cases': path.resolve(__dirname, 'use-cases'),
        '@infra': path.resolve(__dirname, 'infra'),
        '@main': path.resolve(__dirname, 'main'),
      },
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
          },
        },
        { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      ],
    },
    plugins: [new HtmlWebpackPlugin({ template: './index.html' })],
    devServer: {
      static: path.resolve(__dirname, 'public'),
      port: 5173,
      historyApiFallback: true,
    },
  },
  {
    name: 'server'
    entry: './main/server.js',
    target: 'node',
    output: {
      path: path.resolve(__dirname, 'dist', 'server'),
      filename: 'bundle.server.js',
      clean: true,
    },
    resolve: {
      extensions: ['.js'],
      alias: {
        '@domain': path.resolve(__dirname, 'domain'),
        '@use-cases': path.resolve(__dirname, 'use-cases'),
        '@infra': path.resolve(__dirname, 'infra'),
        '@main': path.resolve(__dirname, 'main'),
      },
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
          },
        },
      ],
    },
    devServer: {
      port: 5174,
      historyApiFallback: true,
    },
  }
]

The webpack.config.js now uses a multi configuration set for the application building. To run arch-unit-js using a specific configuration from the webpack file use the webpack.names which is a way to tell arch-unit-js which configurations are going to be used in the aliases resolution during the test. To ilustrate let's use the same example where we want the files inside the **/use-cases/** to depend on the **/domain/** files !

Important: To use this feature, the webpack.names from app(options) must match the key name from the webpack.config.js file !

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'], // Positive Glob pattern, where you specify all extension types your application has
  includeMatcher: ['<rootDir>/**'], // Positive Glob pattern, where you specify all files and directories based on the project <rootDir>
  ignoreMatcher: ['!**/node_modules/**'], // (Optional) - Negative Glob pattern, where you specify all files and directories you do NOT want to check
  webpack: {
    path: '<rootDir>/webpack.config.js', // Path to project 'webpack.config.js' - (using <rootDir> as wildcard)
    names: ['server'], // Array of webpack config names from a 'webpack.config.js' file with multiple configurations
  },
};

// We are using Jest, but you can use any other testing library
describe('Architecture Test', () => {
  it('"**/use-cases/**" files should depends on "@domain"', async () => {
    await app(options)
      .projectFiles()
      .inDirectory('**/usecases/**')
      .should()
      .dependsOn('**/domain/**')
      .check(); // No need to expect, if the dependency is not found it throws an error
  });
});

Again, you successfully tested you application topology 🥳 , you getting the hang of it !

TypeScript - (Basic Scenario)

arch-unit-js also provides support for typescript. To include typescript support just provide the path to your tsconfig.json using the "typescriptPath"

import { Options } from 'arch-unit-js';

const options: Options = {
  extensionTypes: ['**/*.ts'], // Positive Glob pattern, where you specify all extension types your application has
  includeMatcher: ['<rootDir>/**'], // Positive Glob pattern, where you specify all files and directories based on the project <rootDir>
  typescriptPath: '<rootDir>/tsconfig.json', // Path to project 'tsconfig.json' - (using <rootDir> as wildcard)
};

workspaces

Important: Ensure to install arch-unit-js in the root package.json file to ensure the dependency is hoisted for your monorepo project. If you're sure the dependency will not be hoisted and will be installed in the local project within the monorepo then you can skip the following steps and use the tool as is !

arch-unit-js also provides support for workspaces ! Given you're working in a workspace monorepo project ilustrated below:

project/
├── node_modules/
│   └ arch-unit-js/
├── packages/
│   ├── a/
│   │   ├── src/
│   │   └── package.json
│   ├── b/
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── c/
│       ├── src/
│       └── package.json
└── package.json

Suppose you're testing the topology from package b ! To include support for the workspace you can use the following configuration:

import { Options } from 'arch-unit-js';

const options: Options = {
  workspaceDir: '<rootDir>/packages/b',
  extensionTypes: ['**/*.ts'],
  includeMatcher: ['<rootDir>/packages/b/**'],
  typescriptPath: '<rootDir>/packages/b/tsconfig.json',
};

Or alternatively, you can also use the following configuration:

import { Options } from 'arch-unit-js';

const options: Options = {
  workspaceDir: '<rootDir>/packages/b',
  extensionTypes: ['**/*.ts'],
  includeMatcher: ['<workspaceDir>/**'],
  typescriptPath: '<workspaceDir>/tsconfig.json',
};

Note: The annotation <workspaceDir> works as an alias towards the path described in the Options.workspaceDir !

:notebook: API Documentation

app(options)

When checking your architecture you need to test against your application and some of them have different folder structures. And here's where app comes to play.

The initial app API is the representation of your application and to define which files compose your application, you can use as parameter the 'options' to compose your application.

const { app } = require('arch-unit-js');

app({
  workspaceDir: '<rootDir>/packages/a', // Optional,
  extensionTypes: ['**/*.js'], // Required
  includeMatcher: ['<rootDir>/**'], // Required
  ignoreMatcher: ['!**/node_modules/**'], // Optional
  typescriptPath: '<rootDir>/tsconfig.json', // Optional
  webpack: {
    path: '<rootDir>/webpack.config.js', // Optional
    names: ['client', 'server'], // Optional
  },
});

The 'options' parameter is an object which has:

  • The workspaceDir which is a path like string, representing the path to your workspace you're working on
  • The extensionTypes which is a string[] of glob patterns, representing the allowed extensions which compose your project files
  • The includeMatcher which is a string[] of glob patterns, representing the source directories of your application
  • The ignoreMatcher which is a string[] of glob patterns, representing the resources you want to ignore
  • The typescriptPath which is a path like string, representing the path to your typescript config file
  • The webpack.path which is a path like string, representing the path to your webpack config file
  • The webpack.names which is an array telling which webpack configs to use in a multi config file

Note: All the patterns passed to the ignoreMatcher must have a ! , which indicates the given pattern must be ignored from the application !

Globals

projectFiles()

projectFiles() is the function used every time you want to make a broad test against your project structure. You will use it alongside with a "selector" to choose the files location which will be checked by a "matcher" !

To understand better, let's use an example, where you want to test to check if a file stringUtils.js has less than 50 L.O.C. - (Lines Of Code). Here's how to start.

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/stringUtils.js" file should have less than 50 - L.O.C.', async () => {
  await app(options)
    .projectFiles()
    .inFile('**/stringUtils.js')
    .should()
    .haveLocLessThan(50)
    .check();
});

In this case we have the inFile as the "selector" which selects the files which will be tested by the "matcher" , in this case we are only targeting stringUtils.js. Then we have the should which is a "modifier" which indicates it is a positive test done by the "matcher". And finally there is the haveLocLessThan which is the "matcher" whose going to test if the selected files match the criteria !

Selectors

inDirectories(pattern: string[], excludePattern?: string[])

Use the inDirectories to select different files from multiple directories within your project. The "selectors" chains with the "modifiers" to indicate which will be the "matcher" behavior.

Let's say we have an application and we have the directories **/infra/repositories/** & **/infra/providers/**. We want to enforce the files inside this folders contains only very specific dependencies which are the mysql2 & crypto.

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/infra/repositories/**" & "**/infra/providers/**" should only depends on "mysql2" & "crypto"', async () => {
  await app(options)
    .projectFiles()
    .inDirectories(['**/infra/repositories/**', '**/infra/providers/**'])
    .should()
    .onlyDependsOn(['mysql2', 'crypto'])
    .check();
});

Now, let's imagine the structure from the selected directories changed, and now they use barrel exports which means both have now an index.js file exporting all the files.

But the index.js does not comply with the checking "matcher" rule. For this scenario the inDirectories has a second parameter which is used to exclude files and folders you don't want to be checked by the "matcher".

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/infra/repositories/**" & "**/infra/providers/**" should only depends on "mysql2" & "crypto" , excluding "**/infra/**/index.js"', async () => {
  await app(options)
    .projectFiles()
    .inDirectories(['**/infra/repositories/**', '**/infra/providers/**'], ['!**/infra/**/index.js'])
    .should()
    .onlyDependsOn(['mysql2', 'crypto'])
    .check();
});

Note: All the patterns passed to the excludePattern must have a ! , which indicates the given pattern must be excluded from the "matcher" check !

inDirectory(pattern: string, excludePattern?: string[])

Use the inDirectory to select different files from a single directory within your project. It's behavior is the same as the one described for the inDirectories selector, with the exception the pattern parameter is a single string.

To ilustrate it's behavior let's use an example where we are going to have again a directory **/infra/repositories/** and we want to test it it's files use the mysql2, but to make interesting the files implementation are using mysql2/promise now.

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/infra/repositories/**" should depends on "mysql2/**"', async () => {
  await app(options)
    .projectFiles()
    .inDirectory('**/infra/repositories/**')
    .should()
    .dependsOn('mysql2/**')
    .check();
});

Just like the previous example, let's imagine the structure from the selected directory changed, and now uses barrel exports which means it has an index.js file exporting all the other files. Given this scenario let's exclude the index.ts file from the "selectors" !

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/infra/repositories/**" should depends on "mysql2/*" , excluding "**/infra/repositories/**/index.js"', async () => {
  await app(options)
    .projectFiles()
    .inDirectory('**/infra/repositories/**', ['!**/infra/repositories/**/index.js'])
    .should()
    .dependsOn('mysql2/**')
    .check();
});

inFiles(pattern: string[])

Use the inFiles to select different files from different parts of your project. It's behavior is similar than the one described by inDirectories, with the exception that it is not possible to exclude a given pattern with this "selector" !

To ilustrate it's behavior let's use an example where we wanna check if the files **/domain/entities/user.entity.js & **/domain/entities/address.entity.js depends on uuid & lodash.

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/domain/entities/user.entity.js" & "**/domain/entities/address.entity.js" should depends on "uuid", async () => {
  await app(options)
    .projectFiles()
    .inFiles(['**/domain/entities/user.entity.js', '**/domain/entities/address.entity.js'])
    .should()
    .dependsOn(['uuid', 'lodash'])
    .check();
});

inFile(pattern: string)

Use the inFile to select different files OR a single file within your project. This selector is focused in selecting specific files or single file which match the pattern only, providing better semantics towards the test itself.

To ilustrate it's behavior let's use an example where we want to check if a file **/domain/entities/address.entity.js has more than 80 - L.O.C. - (Lines Of Code).

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/domain/entities/address.entity.js" should have more than 80 - L.O.C.', async () => {
  await app(options)
    .projectFiles()
    .inFile('**/domain/entities/address.entity.js')
    .should()
    .haveLocGreaterThan(80)
    .check();
});

Modifiers

should()

Use the should modifier to indicate to "arch-unit-js" what the "matcher" should test. It chains with the "matcher" to indicate how the selected files will be checked !

We can use the another example, where we want all the files in a directory "utils" should match the name *.utils.js.

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"utils" directory should have all files matching the name "*.utils.js"', async () => {
  await app(options)
    .projectFiles()
    .inDirectory('**/utils/**')
    .should()
    .haveName('*.utils.js')
    .check();
});

shouldNot()

The shouldNot modifier indicates the opposite of should , so it is a negative test modifier which tells the "matcher" the selected files should not match the checked pattern.

The code below test if a file numberUtils.js has 50 L.O.C. or more.

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/numberUtils.js" file should have less than 50 - L.O.C.', async () => {
  await app(options)
    .projectFiles()
    .inFile('**/numberUtils.js')
    .shouldNot()
    .haveLocLessThan(50)
    .check();
});

By using the shouldNot "modifier" the "matcher" behave was modified to check if the selected files had a L.O.C. greater or equal than the specified value !

Aggregators

and()

The and is an "aggregator". An "aggregator" gives the ability to chain "selectors" with other "selectors" & chain "matchers" with other "matchers" creating more complex architecture rules to be validated !

In the example below we wanna check if files inside the **/domain/entities/** & **/services/contracts/** directories & **/shared/utils.js file have more than 30 - L.O.C. - (Lines Of Code) & less than 120 - L.O.C. - (Lines Of Code).

const { app } = require('arch-unit-js');

const options = {
  extensionTypes: ['**/*.js'],
  includeMatcher: ['<rootDir>/**'],
};

it('"**/domain/entities/**" & "**/services/contracts/**" & "**/shared/utils.js" files and directories have more than 30 L.O.C. & ;ess than 120 L.O.C.', async () => {
  await app(options)
    .projectFiles()
    .inDirectories(['**/domain/entities/**', '**/services/contracts/**'])
    .and()
    .inFile('**/shared/utils.js')
    .should()
    .haveLocGreaterThan(30)
    .and()
    .haveLocLessThan(120)
    .check();
});

As demonstrated in the example "aggregators" are a powerful tool to create stronger architecture rules by combinig different "selectors" and "matchers" in more meaningful setences !

Matchers

dependsOn

should

shouldNot

onlyDependsOn

should

shouldNot

haveCycles

should

shouldNot

haveName

should

shouldNot

onlyHaveName

should

shouldNot

haveLocLessThan

should

shouldNot

haveLocLessOrEqualThan

should

shouldNot

haveLocGreaterThan

should

shouldNot

haveLocGreaterOrEqualThan

should

shouldNot

haveTotalProjectCodeLessThan

should

shouldNot

haveTotalProjectCodeLessOrEqualThan

should

shouldNot


:memo: License

This project is under MIT license. See the LICENSE file for more details.


Made with lots of 🔥🔥🔥 by Gabriel Ferrari Tarallo Ferraz