/project-lint

A CLI tool to lint project structure

Primary LanguageTypeScriptMIT LicenseMIT

Project Lint

A small tool for setting up project structure linting configuration and running checks to ensure all files fit the requirements.

The code was inspired by the dead project (MIT License)

Features

  • Validation of project structure (Any files/folders outside the structure will be considered an error).
  • Name case validation.
  • Name regex validation.
  • Inheriting the parent's name (The child inherits the name of the folder in which it is located).
  • Folder recursion (You can nest a given folder structure recursively).
  • Forcing a nested/flat structure for a given folder.
  • Ignoring by pattern or via .gitignore.

To Do

  • Sub-folder level .projectlintrc.
  • URL based extends.
  • Default extends.
  • Updatable regexpPatterns.

Installation

$ npm i project-lint

Usage

Run

$ project-lint -c ./project-structure.yml "./**/*"

Help

$ project-lint -h
Usage: project-lint [options] [pathnames...]

File Structure Validator

Arguments:
  pathnames                   Paths to the files to validate

Options:
  -v, --version               Current project-linter version
  -c, --config <path>         Path to the config file (default: "./.projectlintrc")
  -i, --ignore <patterns...>  Ignore patterns (default: ["node_modules/**"])
  -d, --debug [level]         output extra debugging (default: 0)
  -s, --minimalistic          Minimalistic mode
  -h, --help                  display help for command

Lint Staged

module.export = {
  '**/*': ['project-lint -s'],
};

Examples

Simple example for the structure below:

.
├── ...
├── 📄 .projectlintrc
├── 📄 .eslintrc.json
└── 📂 src
    ├── 📄 index.tsx
    └── 📂 components
        ├── ...
        └── 📄 ComponentName.tsx
root:
  - name: .projectlintrc
    type: file
  - name: .eslintrc.json
    type: file
  - name: src
    type: dir
    children:
      - name: index.tsx
        type: file
      - name: components
        type: dir
        children:
          - name: /^${{PascalCase}}\.tsx$/
            type: file

Advanced example for the structure below, containing all key features:

.
├── ...
├── 📄 .gitignore (contains node_modules and etc...)
├── 📄 .projectlintrc
├── 📄 .eslintrc.json
└── 📂 src
    ├── 📂 legacy
    │   ├── ...
    ├── 📂 hooks
    │   ├── ...
    │   ├── 📄 useSimpleGlobalHook.test.ts
    │   ├── 📄 useSimpleGlobalHook.ts
    │   └── 📂 useComplexGlobalHook
    │       ├── 📁 hooks (recursion)
    │       ├── 📄 useComplexGlobalHook.api.ts
    │       ├── 📄 useComplexGlobalHook.types.ts
    │       ├── 📄 useComplexGlobalHook.test.ts
    │       └── 📄 useComplexGlobalHook.ts
    └── 📂 components
        ├── ...
        └── 📂 ParentComponent
            ├── 📄 parentComponent.api.ts
            ├── 📄 parentComponent.types.ts
            ├── 📄 ParentComponent.context.tsx
            ├── 📄 ParentComponent.test.tsx
            ├── 📄 ParentComponent.tsx
            ├── 📂 components
            │   ├── ...
            │   └── 📂 ChildComponent
            │       ├── 📁 components (recursion)
            │       ├── 📁 hooks (recursion)
            │       ├── 📄 childComponent.types.ts
            │       ├── 📄 childComponent.api.ts
            │       ├── 📄 ChildComponent.context.tsx
            │       ├── 📄 ChildComponent.test.tsx
            │       └── 📄 ChildComponent.tsx
            └── 📂 hooks
                ├── ...
                ├── 📄 useSimpleParentComponentHook.test.ts
                ├── 📄 useSimpleParentComponentHook.ts
                └── 📂 useComplexParentComponentHook
                    ├── 📁 hooks (recursion)
                    ├── 📄 useComplexParentComponentHook.api.ts
                    ├── 📄 useComplexParentComponentHook.types.ts
                    ├── 📄 useComplexParentComponentHook.test.ts
                    └── 📄 useComplexParentComponentHook.ts
$schema: ./node_modules/project-lint/projectlintrc.schema.json
workdir: .
gitignore: true

extends: ./node_modules/project-lint/extends/rules.yml

ignorePatterns:
  - src/legacy

root:
  - ruleId: basic_root
  - name: src
    type: dir
    children:
      - ruleId: hooks_folder
      - ruleId: components_folder

rules:
  basic_root:
    - ruleId: projectlint_root
    - ruleId: git_root
    - ruleId: eslint_root
    - ruleId: webpack_root
    # ... etc, check in `extends` file

  hooks_folder:
    - name: hooks
      type: dir
      children:
        - name: /^use${{PascalCase}}(\.(test|api|types))?\.ts$/
          type: file
        - name: /^use${{PascalCase}}$/
          type: dir
          children:
            - ruleId: hooks_folder # recursion
            - name: /^${{parentName}}(\.(test|api|types))?\.ts$/
              type: file

  component_folder:
    - name: /^${{PascalCase}}$/
      type: dir
      children:
        - name: /^${{parentName}}\.(api|types)\.tsx$/
          type: file
        - name: /^${{ParentName}}(\.(context|test))?\.tsx$/
          type: file
        - ruleId: hooks_folder # recursion
        - ruleId: components_folder # recursion

  components_folder:
    - name: components
      type: dir
      children:
        - ruleId: component_folder

API:

$schema: <string> (optional)

Type checking for your '.projectlintrc'. It helps to fill configuration correctly.

{
  "$schema": "./node_modules/project-lint/projectlintrc.schema.json",
  // ...
}

workdir: <string> (default: .)

Set workdir related to config file.

gitignore: <boolean> (default: false)

Include .gitignore file to ignorePatterns.

ignorePatterns: <string[]> (optional)

.gitignore syntaxed list of files to ignore when running.

ignorePatterns:
  - src/legacy

extends: <string | string[]> (optional)

List of other YAML or JSON files with the rules. It will be recursively included and overridden by the current file. You may extend rules, ignorePatterns, but it won't include gitignore: true.

root: <Rule[]>

Rules of the root of the project (related to workdir).

rules: <{ [ruleId]: Rule[] }>

List of named rules may be used via ruleId rule. It may include itself recursively.

Rules

Named file

File rule must contain 2 fields: name and type: file.

name: <string | RegExp>

- name: filename.ts
  type: file
- name: /^file${{PascalCase}}\.ts$/
  type: file

Named folder/dir

Folder rule must contain 2 fields: name and type: file. All the included content may be described in other 2 fields: ignoreChildren or children.

name: <string | RegExp>

- name: dirname
  type: dir
- name: /^dir${{PascalCase}}\.ts$/
  type: dir

ignoreChildren: <boolean>

All the included files will pass successfully.

- name: dirname
  type: dir
  ignoreChildren: true

children: <Rule[]>

All content describes as regular.

allowAny: <boolean>

All not described contents in folder will be ignored. It is useful when we need to ignore a single level but need to check described folders.

root:
  - name: src
    type: dir
    children:
      # rules...

  - allowAny: true

ruleId: <ruleId>

To get any of described in rules section.

root:
  - ruleId: test

rules:
  test:
    - name: src
      type: dir
      ignoreChildren: true

Built-in regex parameters

${{PascalCase}}

Define a pascal case in RegExp name.

- name: /^use${{PascalCase}}\.ts$/
  type: file

Expecting any file called like useMyHook.ts.

${{camelCase}}

Define a camel case in RegExp name.

- name: /^${{camelCase}}Service\.ts$/
  type: file

Expecting any file called like myApiService.ts.

${{kebab-case}} (alias ${{dash-case}})

Define a kebab case in RegExp name.

- name: /^${{kebab-case}}\.d\.ts$/
  type: file

Expecting any file called like module-federation.d.ts.

${{snake_case}}

Define a snake case in RegExp name.

- name: /^${{snake_case}}\.css$/
  type: file

Expecting any file called like i_am_beautiful.css.

${{parentName}}

Use the same name as the parent folder, but the first letter must be lowercase.

- name: TheGodfather
  type: dir
  children:
    - name: /^${{parentName}}\.component\.tsx$/
      type: file
- name: theShavka
  type: dir
  children:
    - name: /^${{parentName}}\.test\.tsx$/
      type: file

Will expect the following:

.
├── 📂 TheGodfather
│   ├── 📄 theGodfather.component.ts
└── 📂 theShavka
    └── 📄 theShavka.test.ts

${{ParentName}}

Use the same name as the parent folder, but the first letter must be uppercase.

- name: Batman
  type: dir
  children:
    - name: /^${{ParentName}}\.must-suffer\.asm$/
      type: file
- name: robin
  type: dir
  children:
    - name: /^${{ParentName}}\.stay-strong\.yaml$/
      type: file

Will expect the following:

.
├── 📂 Batman
│   ├── 📄 Batman.must-suffer.asm
└── 📂 robin
    └── 📄 Robin.stay-strong.yaml

Configuration file

It is YAML by default and must be called .projectlintrc. But you may use json syntax instead.

Contrubution

The more MB of a bundle we have -- the more features we deliver. It works that way, I believe.