-
-
Notifications
You must be signed in to change notification settings - Fork 5
project‑structure file‑composition
Compose your ideal files!
Have full control over the order and quantity of selectors.
Define advanced naming conventions and prohibit the use of specific selectors in given files.
- File composition validation.
- Supported selectors:
class
,function
,arrowFunction
,type
,interface
,enum
,variable
,variableExpression
. - Inheriting the filename as the selector name. Option to add your own prefixes/suffixes, change the case, or remove parts of the filename.
- Prohibit the use of given selectors in a given file. For example,
**/*.consts.ts
files can only contain variables,**/*.types.ts
files can only contain interfaces and types. - Define the order in which your selectors should appear in a given file. Support for
--fix
to automatically correct the order. - Rules for exported selectors, selectors in the root of the file and nested/all selectors in the file. They can be used together in combination.
- Enforcing a maximum of one main function/class per file.
- The ability to set a specific limit on the occurrence of certain selectors in the root of a given file.
- Selector name regex validation.
- Build in case validation.
- Different rules for different files.
- An option to create a separate configuration file with TypeScript support.
🎮Playground for eslint-plugin-project-structure rules.
Check the latest releases and stay updated with new features and changes.
Become part of the community!
Leave a ⭐ and share the link with your friends.
- If you have any questions or need help creating a configuration that meets your requirements, help.
- If you have found a bug or an error in the documentation, report issues.
- If you have an idea for a new feature or an improvement to an existing one, ideas.
- If you're interested in discussing project structures across different frameworks or want to vote on a proposed idea, discussions.
npm install --save-dev eslint-plugin-project-structure
yarn add --dev eslint-plugin-project-structure
pnpm add --save-dev eslint-plugin-project-structure
Add the following lines to eslint.config.mjs
.
Note
The examples in the documentation refer to ESLint's new config system. If you're interested in examples for the old ESLint config, you can find them in the 🎮playground for eslint-plugin-project-structure rules.
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import { projectStructurePlugin } from "eslint-plugin-project-structure";
import { fileCompositionConfig } from "./fileComposition.mjs";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
plugins: {
"project-structure": projectStructurePlugin,
},
rules: {
// If you have many rules in a separate file.
"project-structure/file-composition": ["error", fileCompositionConfig],
// If you have only a few rules.
"project-structure/file-composition": [
"error",
{
// Config
},
],
},
}
);
Create a fileComposition.mjs
in the root of your project.
Warning
Remember to include // @ts-check
, otherwise type checking won't be enabled.
Note
fileComposition.json
, fileComposition.jsonc
and fileComposition.yaml
are also supported. See an example in the 🎮playground for eslint-plugin-project-structure rules.
// File transformUserData.ts
// Let's assume we want all `.ts` files to have the following rules:
// In the root of the file, there may be an `interface` or a `type` that must follow `{FileName}Props`, or simply `Props` if it is not exported.
// This selector must always be in the 0 position in the file, excluding imports.
export type TransformUserDataProps = {
name: number;
surname: number;
email: string;
};
// In the root of the file, there may be an `interface` or a `type` that must follow `{FileName}Return`, or simply `Return` if it is not exported.
// This selector must always be in the 1 position in the file, excluding imports.
interface Return {
fullName: string;
email: string;
}
// In the root of the file, there may be only one main `arrowFunction` that must follow `{fileName}`.
// This selector must always be in the 2 position in the file, excluding imports.
export const transformUserData = ({
name,
surname,
email,
}: TransformUserDataProps): Return => {
// Nested arrowFunctions must follow 'camelCase'.
const nestedFunction = () => {};
// Nested variables must follow 'camelCase'.
const nestedVariable = "";
//All nested selectors that are not specified in the rules are allowed.
return {
fullName: `${name} ${surname}`,
email,
};
};
// All root/exported selectors that are not specified in the rules are prohibited.
// The file may contain at most one main `arrowFunction` and at most two `types`/`interfaces`.
// @ts-check
import { createFileComposition } from "eslint-plugin-project-structure";
export const fileCompositionConfig = createFileComposition({
filesRules: [
{
filePattern: "**/*.ts",
allowOnlySpecifiedSelectors: {
fileRoot: true,
fileExport: true,
nestedSelectors: false,
},
rootSelectorsLimits: [{ selector: ["interface", "type"], limit: 2 }],
rules: [
{
selector: ["interface", "type"],
scope: "fileExport",
positionIndex: 0,
format: "{FileName}Props",
},
{
selector: ["interface", "type"],
scope: "fileRoot",
positionIndex: 0,
format: "Props",
},
{
selector: ["interface", "type"],
scope: "fileExport",
positionIndex: 1,
format: "{FileName}Return",
},
{
selector: ["interface", "type"],
scope: "fileRoot",
positionIndex: 1,
format: "Return",
},
{
selector: "arrowFunction",
scope: "fileExport",
positionIndex: 2,
format: "{fileName}",
},
{
selector: ["arrowFunction", "variable"],
scope: "nestedSelectors",
format: "{camelCase}",
},
],
},
],
});
A place where you can add rules for a given file.
This way, you can ignore specific folders/files:
{
"filesRules": [
// Ignore all files from the `legacy` folder.
{ "filePattern": "legacy/**" },
{
// All `.ts` files from the `components` folders or all `.js` files from the `helpers` folders.
"filePattern": ["**/components/*.ts", "**/helpers/*.js"],
"rules": [
{
"selector": "variable",
"format": "{SNAKE_CASE}"
}
]
}
]
}
Warning
The order of rules matters! Rules are checked in order from top to bottom.
{
"filesRules": [
{
"filePattern": "*",
"rules": [
{
"selector": "variable",
"format": "{SNAKE_CASE}"
}
]
},
{
// File rule with "**/*.consts.ts" will not be taken into account because file rule with "*" meets the condition.
"filePattern": "**/*.consts.ts",
"rules": [
{
"selector": "variable",
"format": "{snake_case}"
}
]
}
]
}
{
"filesRules": [
{
// File rule with "**/*.consts.ts" will be taken into account because file rule with "*" is below file rule with "**/*.consts.ts".
"filePattern": "**/*.consts.ts",
"rules": [
{
"selector": "variable",
"format": "{snake_case}"
}
]
},
{
"filePattern": "*",
"rules": [
{
"selector": "variable",
"format": "{SNAKE_CASE}"
}
]
}
]
}
{
"filesRules": [
{
"filePattern": [["*", "!(**/*.consts.ts)"]],
"rules": [
{
"selector": "variable",
"format": "{SNAKE_CASE}"
}
]
},
{
// File rule with "**/*.consts.ts" will be taken into account because file rule with [["*", "!(**/*.consts.ts)"]] ignores "**/*.consts.ts" pattern.
"filePattern": "**/*.consts.ts",
"rules": [
{
"selector": "variable",
"format": "{snake_case}"
}
]
}
]
}
Here you define which files should meet the rules.
The outer array checks if any pattern meets the requirements. The inner array checks if all patterns meet the requirements.
You can use all micromatch functionalities.
// Name rules for all `.ts`,
{ "filePattern": "**/*.ts" }
// Name rules for all `.js` files or all `.ts` files.
{ "filePattern": ["**/*.js", "**/*.ts"] }
// Name rules for all `.js` files or all `.ts` files except `index.ts`
{ "filePattern": ["**/*.js", ["**/*.ts", "!(**/index.ts)"]] }
The ability to set a specific limit on the occurrence of certain selectors in the root of a given file.
{
"filePattern": "**/*.tsx",
"rootSelectorsLimits": [
// The number of all arrowFunctions in the root of the file cannot exceed 1.
{ "selector": "arrowFunction", "limit": 1 },
// The number of all types and interfaces in the root of the file cannot exceed 2.
{ "selector": ["interface", "type"], "limit": 2 }
]
}
With allowOnlySpecifiedSelectors
, you can prohibit the use of selectors that you haven’t explicitly specified for the file.
The default value is false
.
{
"filePattern": "*",
"allowOnlySpecifiedSelectors": true
}
{
"filePattern": "*",
"allowOnlySpecifiedSelectors": {
"fileExport": true, // Default value, you do not need to specify it.
"fileRoot": true, // Default value, you do not need to specify it.
"nestedSelectors": false
}
}
You can define global errors or precise ones for a given scope.
{
"filePattern": "*",
"allowOnlySpecifiedSelectors": {
"errors": {
"function": "We prefer using arrow functions ..."
},
"fileExport": {
"arrowFunction": "Exporting arrowFunctions is prohibited ..."
},
"fileRoot": {
"arrowFunction": "arrowFunctions in the root of the file are prohibited ..."
},
"nestedSelectors": {
"arrowFunction": "Nested arrowFunctions are prohibited ..."
}
}
}
The place where you add the rules that selectors for a given file must follow.
{
"filePattern": "*",
"rules": []
}
Here you define the selector or selectors you are interested in.
Available selectors:
class
function
arrowFunction
type
interface
enum
variable
variableExpression
{
"filePattern": "**/*.tsx",
"rules": [
{ "selector": ["function", "arrowFunction"] },
{ "selector": "variable" },
{ "selector": "variableExpression" }
]
}
You can restrict variableExpression
to specific names.
limitTo
is treated as a regular expression.
The following improvements are automatically added to the regular expression:
- Regular expression is automatically wrapped in
^$
. - All
*
characters will be converted to(([^/]*)+)
(wildcard). If you want original behavior, use the following notation**
.
{
"filePattern": "**/*.tsx",
"rules": [
// variableExpression with name `styled` should follow {PascalCase}.
{
"selector": { "type": "variableExpression", "limitTo": "styled" },
"format": "{PascalCase}"
},
// variableExpression with name `css` should follow {camelCase}.
{
"selector": {
"type": "variableExpression",
"limitTo": ["css"]
},
"format": "{camelCase}"
},
// All variableExpressions except `styled` and `css` should follow {snake_case}.
{
"selector": {
"type": "variableExpression",
"limitTo": "(?!^(styled|css)$)*"
},
"format": "{snake_case}"
}
]
}
const VariableName = styled.div``;
const variableName = css();
const variable_name = someFn();
Here you define the scope of your rule.
Available options: fileExport
, fileRoot
, nestedSelectors
, file
.
-
file
(default) refers to all selectors in the file and is a shorthand notation for["fileExport", "fileRoot", "nestedSelectors"]
. -
fileExport
refers to exported selectors in the file. -
fileRoot
refers to non-exported selectors in the root of the file. -
nestedSelectors
refers to selectors nested within functions and classes.
{
"filePattern": "**/*.tsx",
"rules": [
{ "selector": "arrowFunction", "scope": ["fileExport", "fileRoot"] },
{
"selector": "variable",
"scope": "file" // Default value, you do not need to specify it.
},
{ "selector": "interface", "scope": "fileRoot" }
]
}
You can define the order in which your selectors should appear in the root of the given file.
The positionIndex
dynamically adjusts to the number of selectors in the file.
All positionIndex
selectors with non-unique names will be sorted alphabetically, taking numbers into account. It is possible to disable the default sorting by setting positionIndex.sorting = "none"
if your selectors are not hoisted.
- If you provide a positive
positionIndex
, you determine the order from the beginning of the file. - If you provide a negative
positionIndex
, you determine the order from the end of the file. - All selectors that do not have a
positionIndex
will be moved below those with a positivepositionIndex
or above those with a negativepositionIndex
.
{
"filePattern": "**/*.tsx",
"rules": [
{
"selector": "interface",
"scope": ["fileExport", "fileRoot"],
"positionIndex": 0,
"format": "{PascalCase}Props"
},
{
"selector": "interface",
"scope": ["fileExport", "fileRoot"],
"positionIndex": 1,
"format": "{PascalCase}Return"
},
{
"selector": "arrowFunction",
"scope": "fileExport",
"positionIndex": 2,
"format": "{FileName}"
},
{
"selector": "variable",
"scope": "fileRoot",
"positionIndex": { "index": 3, "sorting": "none" },
"format": "{camelCase}"
}
{
"selector": "variable",
"scope": "fileExport",
"positionIndex": -1,
"format": "SpecialLastVariable"
}
]
}
Useful if you use prefixes in your filenames and don't want them to be part of the selector name.
Note
Only taken into account when using references
with filename.
{
"filePattern": "**/*.tsx",
"rules": [
{
"selector": "arrowFunction",
"filenamePartsToRemove": ".react", // ComponentName.react.tsx => ComponentName.tsx
"format": "{FileName}" // const ComponentName = () => {}
}
]
}
The format that the given selector must adhere to.
It is treated as a regular expression. If the selector name matches at least one regular expression, it will be considered valid.
The following improvements are automatically added to the regular expression:
- Regular expression is automatically wrapped in
^$
. - All
*
characters will be converted to(([^/]*)+)
(wildcard). If you want original behavior, use the following notation**
.
Note
If you do not specify format
, the default value is {camelCase}.
{
"filePattern": "**/*.tsx",
"rules": [
{
"selector": "arrowFunction",
// Arrow functions in `.tsx` files should meet {camelCase} or {PascalCase}.
"format": ["{camelCase}", "{PascalCase}"]
},
{
"selector": "variable",
// Variables in `.tsx` files should meet `use*` for example `useValue`, `useSomeName`.
"format": "use*"
}
]
}
A place where you can add your own regex parameters.
You can use built-in regex parameters. You can overwrite them with your logic, exceptions are filename references.
You can freely mix regex parameters together see example.
{
"regexParameters": {
"yourRegexParameter": "(Regex logic)",
"camelCase": "(Regex logic)", // Override built-in camelCase.
"fileName": "(Regex logic)", // Overwriting will be ignored.
"FileName": "(Regex logic)" // Overwriting will be ignored.
}
}
Then you can use them in format with the following notation {yourRegexParameter}
.
{ "format": "{yourRegexParameter}" }
{fileName}
Take the name of the file you are currently in and change it to camelCase
.
{ "format": "{fileName}" }
{FileName}
Take the name of the file you are currently in and change it to PascalCase
.
{ "format": "{FileName}" }
{file_name}
Take the name of the file you are currently in and change it to snake_case
.
{ "format": "{file_name}" }
{FILE_NAME}
Take the name of the file you are currently in and change it to SNAKE_CASE
.
{ "format": "{FILE_NAME}" }
{camelCase}
Add camelCase
validation to your regex.
The added regex is ([a-z]+[A-Z0-9]*[A-Z0-9]*)*
.
Examples: component
, componentName
, componentName1
, componentXYZName
, cOMPONENTNAME
.
{ "name": "{camelCase}" }
{PascalCase}
Add PascalCase
validation to your regex.
The added regex is ([A-Z]+[a-z0-9]*[A-Z0-9]*)*
.
Examples: Component
, ComponentName
, ComponentName1
, ComponentXYZName
, COMPONENTNAME
.
{ "name": "{PascalCase}" }
{strictCamelCase}
Add strictCamelCase
validation to your regex.
The added regex is [a-z][a-z0-9]*(([A-Z][a-z0-9]+)*[A-Z]?|([a-z0-9]+[A-Z])*|[A-Z])
.
Examples: component
, componentName
, componentName1
.
{ "name": "{strictCamelCase}" }
{StrictPascalCase}
Add StrictPascalCase
validation to your regex.
The added regex is [A-Z](([a-z0-9]+[A-Z]?)*)
.
Examples: Component
, ComponentName
, ComponentName1
.
{ "name": "{StrictPascalCase}" }
{snake_case}
Add snake_case
validation to your regex.
The added regex is ((([a-z]|\d)+_)*([a-z]|\d)+)
.
Examples: component
, component_name
, component_name_1
.
{ "format": "{snake_case}" }
{SNAKE_CASE}
Add SNAKE_CASE
validation to your regex.
The added regex is ((([A-Z]|\d)+_)*([A-Z]|\d)+)
.
Examples: COMPONENT
, COMPONENT_NAME
, COMPONENT_NAME_1
.
{ "format": "{SNAKE_CASE}" }
Here are some examples of how easy it is to combine regex parameters.
// useNiceHook
// useNiceHook_api
// useNiceHook_test
{ "format": "use{PascalCase}(_(test|api))?" }
// someFileName_hello_world
// someFileName_hello_world_test
// someFileName_hello_world_api
{ "format": "{fileName}_{snake_case}(_(test|api))?" }
A big thank you to all the sponsors for your support! You give me the strength and motivation to keep going!
Thanks to you, I can help others create their ideal projects!