Zen's Hermitage

ESLint Plugin Development Journal for FSD

challengeprojectfsdopensource
πŸ€”This is an open source project currently deployed and maintained.

You can use it from the links below.



Background

Hanghae Plus Curriculum
Hanghae Plus Curriculum

There is a 'Clean Code' course in the Hanghae Plus program.

While taking the course, I learned key factors for refactoring and methods to improve code quality.

During the process, I learned about and applied the concept of Feature-Sliced-Design (FSD).

Since there are many enforced rules, I tried to apply them in my projects and in the real-world work I'm experiencing as a NAVER Webtoon intern, but I felt inconvenienced by the lack of a proper Lint plugin.

So, I decided to develop one.

Problem Situation

If you're going to use a tool, it's better to utilize something that already exists rather than unconditionally trying to create one.

This is one of my development philosophies. One of the advantages of software is 'reusability'. So, I always go through a research process.

The official documentation recommended a tool called Steiger.

However, it didn't have the configuration I needed, and since it's a separate library rather than ESLint, there was the difficulty of having to install and manage it.

Comparison ItemSteiger (Separate CLI Tool)ESLint Plugin
Installation and Maintenance- Requires separate tool (client/server) installation in the project
- Requires additional dependency and version management
- Need to notify entire team for reinstallation when tool updates
- Just add a plugin to existing ESLint configuration
- Reduced burden of separate tool management
- Only need to worry about ESLint version management
Development Workflow and Editor Integration- Since dedicated CLI needs to be run, immediate feedback within IDE may be difficult
- There may be Steiger-related plugins for each editor, but they may not be universal
- ESLint integrates immediately with almost all IDEs like VSCode, WebStorm
- Can check lint errors in real-time
- Automatic checking possible in GitHooks/CI pipelines without separate setup
Rule Control and Granularity (Lint Rules)- Must use based on provided rules
- Rule granularity and direct customization (except open source contributions) are limited
- Being CLI-based, autofix feature within editor may be difficult to use
- Can utilize ESLint's Rule system as-is
- Rule granularity and severity settings like error/warning are flexible, exception handling is easy to configure
- Deep rule writing possible through AST analysis
- Can utilize automatic fixes like ESLint autofix
Onboarding and Training- Need to additionally guide new team members on Steiger tool installation and usage
- Learning burden of separate tool for those who only have ESLint experience
- Most developers are familiar with ESLint
- Minimal learning burden as it only requires guidance like "ESLint rules have been added"
- Migration and team consensus are easy
Extensibility and Customization- Need to consider options and versions provided by the tool itself
- Must wait for tool updates when advanced features or modifications are needed
- Open source contribution process can be complicated
- Quick feedback and contributions possible when released as open source
- Easy integration with @typescript-eslint/parser for TypeScript extensions
- Easy to add/modify rules according to project requirements

For the above reasons, I researched ESLint plugins.

In the process of searching, I found several tools. However, each of them had problems.

ToolDescriptionInconveniences
Eslint plugin for FSD best practicesAn ESLint made based on FSD BP.Last maintenance was 4-5 years ago, and it's not being properly managed.

As a result, it lacked support for Flat Config which is enforced in the latest versions of ESLint 9 and above.
ESLint plugin fsd importIt seemed like a Lint made for personal use.It's not being properly managed and the functionality is limited.

It had low reliability and was difficult to use.
@feature-sliced/eslint-configA Lint for FSD with 135 stars and downloaded by over 8000 people.Proper management was last done in 2022, and Flat Config support wasn't available.

Besides these, I looked for various other tools, but there was no plugin that perfectly fit my needs.

Among the necessary elements, the core part that needed to be addressed was support for Flat Config.

Announcing that support for ESLint versions before 8.x will be discontinued.
Announcing that support for ESLint versions before 8.x will be discontinued.

Especially, since the official documentation mentioned that support for ESLint versions before 8.x would be discontinued, new projects needed to use ESLint 9 or higher.

In the method I found, if a specific ESLint Plugin doesn't provide Flat Config, there was no way to apply it to Flat Config without modifying the plugin's code itself.

So, I decided to create an ESLint Plugin directly to solve this problem.

Project Goals

  • Create an ESLint Plugin that complies with Feature-Sliced-Design conventions.
  • Support Flat Config.
  • Understand how ESLint works while creating the ESLint Plugin.

In fact, I had only been using existing configurations without properly understanding ESLint.

So through this challenge, I decided to properly understand ESLint and create an ESLint Plugin that complies with Feature-Sliced-Design conventions.

Plan

Since this was a challenge I was attempting for the first time, everything was new and everything was a barrier.

Since there were many things I was lacking, I needed to approach this strategically.

I established goals and made plans to effectively achieve them.

PlanContent
1. Understanding Basic Concepts and Environment Preparation- Learn the basic concepts and how ESLint works.
- Basic environment setup for creating ESLint plugins.
- Learn basics of NPM package and module management.
2. Understanding the Basic Structure of ESLint Rules- Try writing simple custom rules.
- Understand ESLint's AST (Abstract Syntax Tree) and ESLint Rule API.
3. Understanding Feature-Sliced Design,
Designing and Implementing Related Rules
- Learn the structure and philosophy of Feature-Sliced Design.
- Conceptualize how to convert design principles into ESLint rules.
- Draft rules for FSD.
4. ESLint Plugin Development- Package multiple rules as a plugin.
- Test and debug the plugin.
5. Deployment Preparation and Testing- Write plugin documentation.
- Learn the process of deploying the plugin to NPM.
6. Test by Applying to Actual Projects- Conduct usability tests to see if it works properly while applying to actual projects.

This is a curriculum I created with GPT. I decided to slowly follow this and proceed with the project.

This article will also proceed according to this process.

Stage 1: Understanding Basic Concepts and Environment Preparation

Goals

  • Understand the roles and how ESLint and Prettier work.
  • Set up the basic environment for ESLint plugin development.
  • Learn package management and development environment setup using NPM.

What is ESLint?

Before we begin, let's briefly look at what ESLint is.

Find and fix problems in your javascript code.

Excerpt from ESLint official documentation

As you can see from the description above, it's a tool that finds and fixes problems in JavaScript code.

ESLint statically analyzes your code to quickly find problems. It is built into most text editors and you can run ESLint as part of your continuous integration pipeline.

Excerpt from ESLint official documentation

ESLint aims to statically analyze code to quickly find problems and maintain consistent code quality.

It's not dependent on libraries or frameworks and can be used in most text editors.

Also, ESLint can be used in CI pipelines.


Summary of ESLint's Main Features

The main features of ESLint can be summarized as follows.

FeatureDescription
Syntax Error DetectionCatches possible errors in code.
Code Style CheckHelps maintain team rules and code style.
Auto-fixAutomatically fixes problems when possible.
ExtensibleCan be extended as desired by adding plugins or rules.

Why ESLint is Important

ESLint plays an important role in maintaining code quality and keeping code conventions consistent among team members.

Especially in large-scale projects, such tools are essential.

It reduces debugging time and prevents unnecessary resource waste related to conventions.

In other words, it's important because it's a tool that increases productivity.


How ESLint Works

ESLint works through the following process.

1. Code Input

ESLint reads in JavaScript or TypeScript files to inspect.

2. Parsing

To analyze the code, it uses a Parser to convert it into an AST (Abstract Syntax Tree).

By default, ESLint uses a parser called espree, but to support TypeScript, you can also use custom parsers like @typescript-eslint/parser.

AST is a data structure that represents code structure in tree form.

JavaScript

As an example, this code is converted to the following AST.

Program
 β”œβ”€β”€ VariableDeclaration (const sum)
 β”‚    β”œβ”€β”€ VariableDeclarator
 β”‚    β”‚    β”œβ”€β”€ Identifier (sum)
 β”‚    β”‚    β”œβ”€β”€ ArrowFunctionExpression
 β”‚    β”‚         β”œβ”€β”€ Parameters (a, b)
 β”‚    β”‚         β”œβ”€β”€ Body (BinaryExpression +)

3. Applying Rules

ESLint traverses the AST and applies configured Rules.

Rules operate according to Node Type, and when specific patterns are found, they can report errors or automatically fix them.

For example, if the no-console rule is enabled, ESLint will display a warning if there's code like console.log().

At this time, ESLint uses the Visitor Pattern to inspect rules for each node.

JavaScript

This rule is a simple example where ESLint displays a warning when using a variable called temp.

πŸ€”Why ESLint Uses the Visitor Pattern

To Add New Functionality (Inspection Rules) While Maintaining Objects (Elements)

ESLint does not directly modify code (AST).

Instead, it defines visitor functions inside create(context) so that new functionality (rule application) can be performed on specific nodes (objects).

To Perform Specific Tasks While Visiting Each Node Type

In the Visitor Pattern, the Visitor object traverses objects (Elements) and performs specific tasks. In ESLint, methods that can visit each AST node type are provided.

In the example code, the Identifier(node) function plays the role of a Visitor that is executed when visiting an Identifier (variable name) node in the AST.

4. Reporting

  • ESLint outputs the inspection results to the console or file.
src/index.js
  2:5  error  Unexpected console statement  no-console
  • 2:5 β†’ Problem occurred at line 2, position 5
  • error β†’ Error level
  • Unexpected console statement β†’ Message defined in the rule
  • no-console β†’ Applied ESLint rule name

5. Auto-fixing

  • Some rules can be automatically fixed. (Using --fix option)
Bash

For example, if the semi (semicolon enforcement) rule is applied:

JavaScript

After auto-fix:

JavaScript

Main Components of ESLint

ESLint is composed of the following main components.

1. Parser

  • Responsible for converting code into AST.
  • By default, it uses espree, but to support TypeScript or JSX, a different parser needs to be specified.
    • TypeScript support: @typescript-eslint/parser
    • Babel support: babel-eslint

2. Rule Engine

  • Responsible for executing and applying rules while traversing the AST.
  • ESLint has built-in rules, but custom rules can also be added.
  • User-defined rules can be added through eslint-plugin.

3. Formatter (Output Formatter)

  • Responsible for outputting ESLint results to console or file.
  • By default, the stylish formatter is used, but various formatters like JSON, HTML, Markdown can be used through the --format option.
  • Example) eslint src --format json

4. Fixer (Auto-fixer)

  • When using the --fix option, ESLint automatically fixes some rules.
  • However, logical problems cannot be fixed. (e.g., problems like no-unused-vars)

Role of ESLint Configuration Files

The behavior of ESLint is determined based on configuration files.

JavaScript
  • files: Apply rules for specific file extensions (.ts, .tsx).
  • languageOptions.parser: Use @typescript-eslint/parser for AST (Abstract Syntax Tree) conversion (for TypeScript code analysis).
  • plugins: Add @typescript-eslint/eslint-plugin to enable TypeScript-specific ESLint rules.
  • rules: Set code style and code quality rules.

Additional Concepts for Deep Understanding of ESLint's Operation

1. Code Analysis Using AST (Abstract Syntax Tree)

Since ESLint operates based on AST, directly examining the AST makes rule creation easier.

  • Tools like AST Explorer can be used to visually check the AST of code.

2. Plugins and Extensions

ESLint can extend functionality by adding plugins. (This is the element I want to create through this project.)

  • Example: Adding a React plugin
Bash
  • Add to configuration file
JavaScript
  • files: Apply React-related ESLint rules to **/*.jsx, **/*.tsx files.
  • plugins: Add eslint-plugin-react to enable React-related rules.
  • rules: react.configs.recommended.rules β†’ Apply React recommended rules as-is (same as plugin:react/recommended).

Preparing the Basic Development Environment

1. Installing Node.js

Since ESLint runs in a Node.js environment, Node.js must be installed.

Download and install the LTS version from the Node.js official website.


2. Creating the Project Directory

Create a project directory to develop the ESLint plugin.

Bash

3. NPM Project Initialization

Initialize the NPM project.

Bash

This will create a package.json file. This file manages the project's settings and dependencies.

npm init command execution result
npm init command execution result

4. Installing ESLint

Install ESLint.

Bash

Use the --save-dev option to install as a development dependency. (This means it's only used in the development environment and won't be included in the build.)


5. Initializing ESLint

Initialize ESLint.

Bash

Running this command starts the ESLint initialization setup.

  • "How would you like to use ESLint?" β†’ Select "To check syntax, find problems, and enforce code style".
  • "What type of modules does your project use?" β†’ Select "JavaScript modules (import/export)".
  • "Which framework does your project use?" β†’ Select "None of these" (React can be added later if needed).
  • "Does your project use TypeScript?" β†’ Select "No" (because we're starting with JavaScript).
  • "Where does your code run?" β†’ Select "Node".
  • "Would you like to install them now?" β†’ Select "Yes".
  • "Which package manager do you want to use?" β†’ Select "npm".
ESLint initialization setup
ESLint initialization setup

Installing Prettier

Prettier is a tool that automatically formats code style.

When used with ESLint, you can manage both code quality and style.

Since we'll cover this at the very end, let's just install it for now.

Bash

Designing the Directory Structure

Keep the initial directory structure simple.

eslint-fsd-plugin/
β”œβ”€β”€ node_modules/
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ package.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ src/
└── README.md

Stage 2: Understanding the Basic Structure of ESLint Rules

Goals

  • Understand how ESLint analyzes code (parsing, AST usage).
  • Create a simple ESLint rule yourself.
  • Learn how to apply and run rules.

Recalling ESLint Rules Once More

We already covered in detail how ESLint works earlier, but since it's important content for project progress, let's recall it once more.

ESLint operates based on AST (Abstract Syntax Tree), not simple string comparison.

What is AST?

AST represents the structure of code in tree form.

For example, let's look at the code below.

JavaScript

The AST of this code is roughly represented as follows.

Program
β”œβ”€β”€ VariableDeclaration (const message = "Hello, world!")
β”‚   β”œβ”€β”€ Identifier (message)
β”‚   └── Literal ("Hello, world!")
└── ExpressionStatement (console.log(message))
    β”œβ”€β”€ MemberExpression (console.log)
    └── Identifier (message)

In this way, the structure of code is represented in tree form through AST.

ESLint analyzes code and applies rules using this tree.

Creating a Simple ESLint Plugin

I understand the principle, but I can't quite grasp how to create rules using this.

So, I want to practice by creating a simple rule.

Let's create a rule that prevents using console.log as described in the previous example.

1. Creating Rule Directory

First, create a directory to store rules.

Bash

Here, the -p option automatically creates parent directories.

For example, if you're trying to create the src/rules folder but src doesn't exist, using mkdir -p will automatically create it starting from src.


2. Creating Rule File

Create a file to store the rule.

Bash

3. Writing Rule Code

Now, write the rule code in the no-console-log.js file.

JavaScript

This code performs the following roles.

  1. ESLint finds CallExpression nodes. -> This means function calls.
  2. If console.log is called:
    • node.callee.object.name === "console"
    • node.callee.property.name === "log"
  3. If the above conditions are met, ESLint displays a warning message.

4. Creating a Plugin Based on the Rule

Now it's time to register the rule we created in eslint.config.mjs.

In Flat Config, unlike the existing .eslintrc.js method, plugins must be explicitly registered and rules must be called using namespaces.

JavaScript

As shown above, register the plugin in the plugins property and call rules in the format [plugin-name]/[rule-name].

So even if you created a rule like no-console-log.js, the plugin needs to be configured separately.

Currently we only created a rule, not a plugin.
ESLint requires a plugin for applying custom rules.
Therefore, we need to create a plugin to apply the rule.

(1) Creating a Plugin

You need to wrap rules in plugin format in index.js.

In Flat Config, defining a rules object within the plugin is mandatory.

At this time, the file name doesn't have to be index.js. (e.g., plugin.js)

However, by convention, since it's also the starting point of the plugin, the name index.js was used.

Let's create it as follows.

JavaScript

As shown above, create a rules object to register rules.

The important points here are as follows.

  • Create a rules object to register rules.
  • The rules object must be included when exporting.
  • When set as export default { rules: { "no-console-log": noConsoleLog } }; format, Flat Config will recognize it correctly.

5. Using the Plugin Created in Flat Config

Now you need to clearly register the plugin in eslint.config.mjs and set up the namespace.

Open the file and add the following.

JavaScript
Important Points Here
  • You need to register the plugin in the format fsd: eslintFsdPlugin inside the plugins object.
  • In rules, you must call it with the namespace (fsd/) like "fsd/no-console-log".
    • "no-console-log" β†’ ❌ Error occurs
    • "fsd/no-console-log" β†’ βœ… Correct way

πŸ€”There's already something in eslint.config.mjs...?
JavaScript

There may already be values inside like above.

This is the content described in the Result Description section above,

which is a configuration for applying Node.js environment or basic JavaScript rules.

Keep this configuration as is and add the newly created rules.

JavaScript

Flat Config can apply multiple properties connected in array format.

So you can apply it as shown above.

JavaScript

By the way, the code above represents TypeScript type definitions.

This allows TypeScript to know that the variable is of type Linter.Config[].

If you're not using TypeScript, you can delete it, but since you don't know what might happen, it's recommended to leave it as is.


5. ESLint Execution Test

Now let's create a file containing console.log and run it.

(1) Create Test File
Bash

Then add the following code to the test.js file.

JavaScript
(2) Run ESLint
Bash
(3) Check Execution Result
ESLint Execution Result
ESLint Execution Result

You can confirm that the error is output normally.

6. Debugging When Running npx eslint

After applying all settings correctly, you need to verify that ESLint is loading the plugin normally.

Bash
Points to Check in Debug Results
  1. Loaded plugin: fsd
    • If there is a Loaded plugin: fsd log, the plugin has been loaded normally.
  2. Applying rule: fsd/no-console-log
    • If there is an Applying rule: fsd/no-console-log log, the rule is being applied.
  3. ESLint Execution Result
    test.js
      1:1  error  Do not use console.log.  fsd/no-console-log
    • If the error occurs as shown above, it is working normally.

7. Summary

In this stage, we proceeded with the following content.

StageModificationsDescription
1️⃣ Rule Definition FixSet meta.messages and context.report() correctly in rules/no-console-log.jsmeta information is required in Flat Config
2️⃣ Plugin ConfigurationExport including rules object in index.jsDefined in rules: { "no-console-log": noConsoleLog } format
3️⃣ Apply Flat ConfigRegister as plugins.fsd in eslint.config.mjs and apply rule in "fsd/no-console-log" formatplugins object is required in Flat Config
4️⃣ ESLint DebuggingRun npx eslint --debug src/test.jsConfirm Loaded plugin: fsd and Applying rule: fsd/no-console-log
  1. In Flat Config, ESLint plugins must be registered as a namespace.
    • "no-console-log" β†’ ❌ Error occurs
    • "fsd/no-console-log" β†’ βœ… Correct way
  2. The rules object must be exported as a plugin in index.js.
    • export default { rules: { "no-console-log": noConsoleLog } }; β†’ βœ… Correct way
  3. Before running ESLint, you should verify that the plugin is loaded correctly with npx eslint --debug.
    • Make sure to check for the Loaded plugin: fsd log.

Stage 3: Feature-Sliced Design (FSD) Based ESLint Rule Design and Implementation

So far, we've created a simple ESLint rule.

From now on, let's understand Feature-Sliced Design (FSD) and proceed with designing rules for it.

Goals

  • Review the concepts of Feature-Sliced Design (FSD) once more.
  • Design what role ESLint rules should play in FSD structure.
  • Directly implement basic FSD rules.

What is Feature-Sliced Design (FSD)?

Feature-Sliced Design (FSD) Concept
Feature-Sliced Design (FSD) Concept

FSD is an architecture that structures projects around features to enhance scalability and maintainability.

It's an architecture pattern that maximizes maintainability and scalability by organizing frontend projects around features.

Unlike traditional folder-based structures like pages, components, services, the core is to structure according to business domains and features.

Core Principles of FSD

1. Feature-Oriented Architecture

  • Unlike traditional layer-based structures (e.g., structures where all components and services gather in components/, services/ folders), it follows a method of dividing folders by each feature unit and placing all related elements (UI, state, API calls, logic, etc.) in one place.
  • For example, all elements related to Authentication are placed inside features/auth/, and Payment-related elements are placed in features/payment/.
  • Grouping by feature unit increases reusability and makes maintenance easier.

Like REST API, it has the effect of clearly expressing what the code does through the directory itself.

For example, the features/auth/ directory contains all code related to authentication processing, and the features/payment/ directory contains all code related to payment processing.

In the case of entities/auth/ directory, it contains code related to processing logic related to authentication information.

In this way, there is an advantage that you can easily grasp what function it is responsible for just by looking at the directory name.


2. Layered Architecture

  • FSD consists of 7 layers, and upper layers can reference lower layers, but conversely, lower layers are prohibited from referencing upper layers. (Prevention of upward dependency)
  • βœ… Correct reference relationship
    App β†’ Process β†’ Pages β†’ Widgets β†’ Features β†’ Entities β†’ Shared
  • ❌ Wrong reference examples
    Features β†’ Pages (X)  // Features layer should not reference Pages
    Entities β†’ Features (X)  // Entities should not reference Features

Thanks to this layer structure, FSD can reduce coupling between code and maximize maintainability.


3. Proper Dependency Management (Dependency Rules)

  • In FSD, clear dependency rules must be followed.
  • In particular, the core is to force that upper elements cannot be referenced when importing.

Rule: Cannot reference upper layers
  • Inside features/, you cannot import code from pages/ or widgets/.
  • Inside entities/, you cannot import features/ or widgets/.
  • βœ… Correct import example
TypeScript
  • ❌ Wrong import example
TypeScript

If you follow this principle, lower modules don't depend on upper modules, so coupling becomes lower and it becomes easier to maintain features independently.


4. Domain-Driven Design (DDD)

  • Design by grouping each domain (e.g., user, product, payment, etc.) by feature unit.
  • In traditional MVC patterns, models and services exist separately, but FSD takes an approach similar to domain-driven design by gathering and placing all related elements (UI, state, API, logic, etc.) around features.

This makes code exploration easier and has the advantage of making maintenance simpler by feature unit.


5. Increased Reusability and Modularity

  • Place commonly used utilities, styles, API functions, basic components, etc. in the Shared layer to prevent duplication and make them easily reusable throughout the project.
  • Components, state management, and API requests are modularized by Features unit, enabling independent development.
  • Example:
    /shared/
      β”œβ”€β”€ api/
      β”œβ”€β”€ ui/
      β”œβ”€β”€ utils/
  • For example, a Button component that can be reused on all pages should be located in shared/ui/Button.tsx.

6. Increased Maintainability and Scalability

  • Following FSD's structure makes maintenance easier even as the project grows.
  • When adding new features, you can develop independently without affecting existing code.
  • Refactoring becomes easy β†’ Modifying only specific features doesn't affect the entire project.
  • Team collaboration becomes easier β†’ Work can be divided and progressed by feature unit.

In particular, FSD has very detailed documentation and related examples.

If you adopt this, you can significantly reduce the cost of synchronizing thoughts about architecture within the team.

This is because you just need to make decisions based on detailed documentation, and only need to agree on ambiguous things.


7. Summary

AdvantageDescription
Code readability and maintainability increaseEasy to find because code is organized by feature
Excellent scalabilityThe advantages of FSD are maximized as the project grows
Dependencies become clearReduces coupling by forcing lower layers not to reference upper layers
Domain-centric development becomes possibleApplication fits well with actual business logic
Refactoring becomes easierStable changes are possible because you can modify by feature unit
Collaboration among team members becomes easyWork can be divided and progressed by each feature unit

πŸ’‘Feature-Sliced Design (FSD) Reference Materials

Why ESLint Rules are Needed in FSD

FSD is a method that structures projects around features to increase scalability and maintainability.

However, if developers don't maintain this consistently, it can rather cause confusion.

Problem TypeExampleProblem
❌ Wrong layer importimport app/ from features/features shouldn't know about app, but dependency forms
❌ Wrong path importimport Button from "../../shared/ui/Button";Importing with relative paths (../../) makes maintenance difficult
❌ Wrong inter-slice dependencyfeatures/auth directly imports features/paymentShould be independent of each other but strong dependency forms
❌ Wrong global state accessimport app/store.ts from features/feature shouldn't directly know about global store
❌ Wrong UI element importimport widgets/ from entities/Business logic (entities) shouldn't know about UI

The above problems are mistakes that developers often make.

What Rules Will I Create?

I will create rules that monitor code that breaks FSD principles.

And naturally, these rules will be created based on Flat Config, and I will create plugins to apply them.

Options will be set to error by default.

However, naturally, rules can be modified as the user wishes, so I intend to implement it so that users can warn or off as needed.


List of ESLint Rules I Will Create

Rule NameDescriptionExample (Wrong Code)
forbidden-importsProhibit upper layer import and cross-importfeatures/ β†’ app/ import prohibited
no-relative-importsProhibit relative path import, force using alias like @shared/ui when importingimport Button from "../../shared/ui/Button"; ❌
no-public-api-sidestepProhibit bypassing public API (index.ts) importimport { authSlice } from "../../features/auth/slice.ts"; ❌
no-cross-slice-dependencyProhibit import between feature slices (features/auth β†’ features/payment ❌)import { processPayment } from "../payment"; ❌
no-ui-in-business-logicProhibit UI import in business logic (entities β†’ widgets prevention)import { Header } from "../../widgets/Header"; ❌
no-global-store-importsProhibit direct global store importimport store from "../../app/store.ts"; ❌
ordered-importsImport ordering (grouping by layer)import { Button } from '@shared/ui'; import { User } from '@/entities/user'; ❌
  • Rule names were written with reference to steiger, FSD's linter.
  • Except for forbidden-imports, it was designed to support linting for parts not supported by steiger without overlapping elements.
  • I plan to add content from steiger in the future.

Explanation of Each Rule's Necessity

1. forbidden-imports: Applying Correct Inter-layer Dependencies
  • Prohibits importing lower layers from upper layers.
  • For example, prevents importing app/ from features/.
  • This rule guides adherence to FSD's layer structure.
TypeScript

2. no-relative-imports: Enforcing Path Alias Usage
  • Prohibits using relative paths (../../) and forces using aliases (@shared/ui).
  • This is to improve code readability and make maintenance easier.
TypeScript

3. no-public-api-sidestep: Enforcing Import Through public API
  • Forces not to directly import internal files of each slice (features, entities, widgets).
  • Restricts to always import through index.ts.
TypeScript

4. no-cross-slice-dependency: Limiting Inter-Slice Dependencies
  • Prohibits imports between feature slices.
  • Prevents importing features/payment from features/auth.
  • This is to ensure each feature can operate independently.
TypeScript

5. no-ui-in-business-logic: Limiting UI Layer Imports
  • Restricts business logic (entities) from importing UI (widgets).
TypeScript

6. no-global-store-imports: Limiting Global State Management Access
  • Prohibits directly importing global state (store) from feature, widgets, entities, etc.
  • Global state should be accessed through state management hooks like useStore, useSelector.
  • Applies general rules considering various state management libraries like Redux, Zustand.
  • features/auth/AuthForm.tsx is directly importing the store.
  • Global state should be accessed through hooks, not by directly accessing the store object.

7. ordered-imports: Enforcing Import Statement Grouping
  • Forces grouping of import statements to improve code readability.
  • For example, the goal is to improve code readability by grouping import statements by the same layer.
  • Forces organizing when import order is randomly mixed.
  • You can see that the import ordering is messy.
  • There are problems that it's hard to see at a glance and difficult to maintain.

Implementing ESLint Rules

Now let's implement the rules we designed above.


Implementing the no-relative-imports Rule

Bash

Implementing the no-public-api-sidestep Rule

Bash

Implementing the no-cross-slice-dependency Rule

Bash

Implementing the no-ui-in-business-logic Rule

Bash

Implementing the no-global-store-imports Rule

Bash
πŸ€”Note

Which directory to put global state in depends on the project. Therefore, this needs to be considered per project, and should also be considered based on the global state management elements pursued by FSD.

However, here we assumed a really universal situation and created a rule that prohibits app/store and shared/store.

Since there are cases where the store element goes into model, this rule will be modified in the future.


Implementing the forbidden-imports Rule

LayerImportable Targets
appprocesses, pages, widgets, features, entities, shared βœ…
processespages, widgets, features, entities, shared βœ…
pageswidgets, features, entities, shared βœ…
widgetsfeatures, entities, shared βœ…
featuresentities, shared βœ…
entitiesshared βœ…
sharedDoes not import any layer βœ…

This rule enforces the above structure.

Bash

Implementing ordered-imports Rule

Bash

Step 4: ESLint Plugin Development and Packaging

Let's look at how to develop and package an ESLint plugin.

Goals

  • Bundle individual rules into a single ESLint plugin package.
  • Organize into an NPM-distributable eslint-plugin-fsd format.
  • Verify proper operation through testing and debugging.

Organizing ESLint Plugin Structure

So far we've implemented individual rules in the rules/ folder.

Let's create the following directory structure to organize it as a plugin package.

Delete the no-console-log.js that was included for testing.

eslint-fsd-plugin/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ rules/
β”‚   β”‚   β”œβ”€β”€ forbidden-imports.js
β”‚   β”‚   β”œβ”€β”€ no-relative-imports.js
β”‚   β”‚   β”œβ”€β”€ no-public-api-sidestep.js
β”‚   β”‚   β”œβ”€β”€ no-cross-slice-dependency.js
β”‚   β”‚   β”œβ”€β”€ no-ui-in-business-logic.js
β”‚   β”‚   β”œβ”€β”€ no-global-store-imports.js
β”‚   β”‚   β”œβ”€β”€ ordered-imports.js
β”‚   β”œβ”€β”€ index.js   <-- Plugin entry point
β”œβ”€β”€ tests/         <-- ESLint rule test code
β”œβ”€β”€ package.json
β”œβ”€β”€ eslint.config.mjs (Test Flat Config)
β”œβ”€β”€ README.md      <-- Documentation

You can confirm the directory becomes like the above.

Packaging ESLint Plugin

From now on, this is the preparation process for packaging the ESLint rules I created and distributing them on NPM.

1. package.json Configuration

You need to configure package.json to make the ESLint plugin into a single package.

Add the following content to package.json.

JSON
  • name: Set to eslint-plugin-fsd-lint. (eslint-plugin-xxx format)
  • main: Specify src/index.js as the plugin entry point.
  • type: Set to module.
  • scripts: Add lint and test scripts.
    • lint: Run ESLint.
    • test: Run tests.
  • keywords: Add ESLint, plugin, feature-sliced-design, fsd, lint keywords.
  • author: Add the author. (I wrote my GitHub ID.)
  • license: Apply MIT license.
  • dependencies: Add since ESLint is required.
  • peerDependencies: Add since ESLint is required.

2. Write index.js Plugin Entry Point

JavaScript
  • Register all rules in the rules object so they can be used in the plugin.

Plugin Testing

1. Modify ESLint Configuration File

Modify eslint.config.mjs for testing.

JavaScript
  • Register fsdPlugin in eslint.config.mjs so you can test the ESLint plugin.

2. Add Test File

Add a fsd-rules.test.js file in the tests/ directory.

JavaScript
  • After running ESLint, outputs rule violation messages to proceed with testing.

3. Run Tests

Bash

Preparing Package for NPM Distribution

To distribute on NPM, let's add a .npmignore file.

tests/
node_modules/
.eslintrc.js
eslint.config.mjs
.idea/
.vscode/
.DS_Store/

Step 5: Plugin Documentation and NPM Distribution

Actually, deployment has already been completed.

Now it's time to polish the deployed elements.

Goals

  • Write a README.md file to organize plugin usage instructions and rule explanations.
  • Provide guides so users can easily set up from GitHub and NPM pages.
  • After package deployment is complete, establish version management and update strategies.

Code Localization

To handle the code officially, documentation and localization work is necessary.

So far, we've written comments and error messages based on our native language, Korean.

From now on, we'll remove Korean and change to English, and everything will be handled in English.

JavaScript

Writing Documentation Including README.md

Let's write the plugin usage instructions and rule explanations in README.md.

Since we'll support not only English but also Korean, we plan to write both.

Elements related to this are attached as images.

Write README.md
Write README.md

NPM Distribution

Let's distribute on NPM again based on the documentation we created.

JSON

Bump the version from 1.0.0 to 1.0.1.

NPM and Github Integration

When you integrate NPM and Github, you can use the feature that automatically distributes to NPM when you push code to Github.


Add Github Repository Information to package.json

To connect GitHub and NPM, you need to add repository and bugs information to package.json.

JSON

Bump the version from 1.0.1 to 1.0.2.

  • "repository" β†’ Connect GitHub repository address
  • "bugs" β†’ Add issue tracking URL
  • "homepage" β†’ Add GitHub README.md link

Commit & Push After package.json Update

Reflect the modified package.json to Github.

Bash

NPM Package Update

Redistribute the package with updated information.

Bash

Finally, update the release notes and the related work is complete.

CI/CD Configuration

Let's make it so that when you push a new tag on GitHub and create a Release, it automatically distributes to NPM.

To do this, we'll set up GitHub Actions to configure CI/CD (Continuous Integration/Continuous Deployment).

To distribute to NPM from GitHub Actions, you need an NPM Access Token.

Follow the steps below to issue a Token from NPM.

1️⃣ Go to NPM Official Website 2️⃣ Click profile in the upper right β†’ Select "Access Tokens" 3️⃣ Click "Generate New Token" 4️⃣ Generate "Automation" type Token (needed for automated distribution) 5️⃣ Copy the issued Token (will add to GitHub later)

βœ… Now the NPM Access Token is ready. (Make sure to keep the Token secure so it's not exposed.)

Step 6: Applying to an Actual Project

Let's apply the created Lint to an actual project.

Fortunately, there was a project that actively utilized FSD while working on Hanghae Plus.

There was no other FSD related Lint here, so it was a perfect fit.

I was using FSD but... had no lint... It was an optimal condition.

Install ESLint in Project

Bash

Since it was already deployed, I just needed to download and use it.

This project was using pnpm, so I installed it based on that.

Install ESLint in Project
Install ESLint in Project

You can confirm it was installed successfully.

Configure ESLint in Project

JavaScript

I added the configuration as shown above.

Run ESLint in Project

Bash

I configured it as above and ran the lint.

The results are as follows.

Lint Execution Results
Lint Execution Results

I could confirm it was working correctly.

Lint Fix Execution Results
Lint Fix Execution Results

I could also confirm that fix was working correctly.

Summary

So far, I've created an ESLint plugin.

What started from inconvenience turned out to be more fun than expected.

The scale also grew quite a bit.

Through this, I was able to understand more clearly how ESLint works and what Flat Config is.

Also, this was my first time publishing an NPM package and creating Open Source, which was also a new experience.

It wasn't just about publishing and being done with it - there were related configurations to set up, and since actual deployment was happening, I had to be sensitive to bugs.

Currently the scale is small, but if it grows larger, tests will need to be more rigorous, and lint rules will also need to be stricter.

I made this during the Lunar New Year holiday as a review of existing Hanghae content, and also because I thought it might be needed in my internship work - I think it was a good decision.

My development skills seem to have improved a lot recently after BoostCamp ended. Or more precisely, my spirit of challenge?

It seems like my efforts to not avoid inconvenience and try to solve problems have increased.

I should maintain this state for a while and learn more things.

Future Plans

  • I'm thinking of gradually porting the content supported by Steiger to ESLint.
  • Also, Prettier settings can be customized too, so I'm thinking about making something for that as well... but I have other things to do, so I'll think about it a bit more before trying.