diff --git a/documentation/migration-to-2.1.0.md b/documentation/migration-to-2.1.0.md index af394ca..54db920 100644 --- a/documentation/migration-to-2.1.0.md +++ b/documentation/migration-to-2.1.0.md @@ -6,9 +6,12 @@ A minor configuration fix will be required for version <= 1.4.7. ### General changes: -- The entire documentation has been rewritten for ESLint's new config system. Examples with the old ESLint configuration can be found in the [**playground**](https://github.com/Igorkowalski94/eslint-plugin-project-structure-playground) for eslint-plugin-project-structure rules. +- A shorter notation option for [structure](https://github.com/Igorkowalski94/eslint-plugin-project-structure/blob/main/documentation/project-structure-folder-structure.md#structure). +- New build-in {SNAKE_CASE} regexParameter. +- Improvements for {PascalCase} and {camelCase} regexParameters. +- The entire documentation has been rewritten for ESLint's new config system. Examples with the old ESLint configuration can be found in the [playground](https://github.com/Igorkowalski94/eslint-plugin-project-structure-playground) for eslint-plugin-project-structure rules. - New option for creating a configuration file in an .mjs file with TypeScript support. -- Enforcing the existence of a file/folder when a specific file/folder exists. For example, if `src/Component.tsx` exists, then `src/Component.test.tsx` and `src/stories/Component.stories.tsx` must also exist. +- [Enforcing the existence](https://github.com/Igorkowalski94/eslint-plugin-project-structure/blob/main/documentation/project-structure-folder-structure.md#enforce-existence) of a files/folders when a specific file/folder exists. For example, if `src/Component.tsx` exists, then `src/Component.test.tsx` and `src/stories/Component.stories.tsx` must also exist. - You can now use comments in folderStructure.json and independentModules.json files. - Improved error messages for folder-structure. - Easier configuration of folder-structure. The "extension" key has been removed, now the file extension will be part of the "name". You don't need to add /^$/ to your regex, they will be added automatically and other improvements. @@ -122,17 +125,13 @@ The following improvements are automatically added to the regex: From: ${{key}} ```jsonc -{ - "name": "/^${{parentName}}$/", -} +{ "name": "/^${{parentName}}$/" } ``` To: {key} ```jsonc -{ - "name": "{parentName}", -} +{ "name": "{parentName}" } ``` ### Changes for build-in PascalCase @@ -169,9 +168,7 @@ Add **`SNAKE_CASE`** validation to your regex.
The added regex is **`((([A-Z]|\d)+_)*([A-Z]|\d)+)`**. ```jsonc -{ - "name": "{SNAKE_CASE}", -} +{ "name": "{SNAKE_CASE}" } ``` ### New rules: diff --git a/documentation/project-structure-folder-structure.md b/documentation/project-structure-folder-structure.md index 28da06f..9843212 100644 --- a/documentation/project-structure-folder-structure.md +++ b/documentation/project-structure-folder-structure.md @@ -8,7 +8,7 @@ Enforce rules on folder structure to keep your project consistent, orderly and w ✅ File/Folder name regex validation with features like wildcard `*` and treating `.` as a character, along with other conveniences.
✅ Build in case validation.
✅ Inheriting the folder's name. The file/folder inherits the name of the folder in which it is located. Option of adding your own prefixes/suffixes or changing the case.
-✅ Enforcing the existence of a file/folder when a specific file/folder exists. For example, if `./src/Component.tsx` exists, then `./src/Component.test.tsx` and `./src/stories/Component.stories.tsx` must also exist.
+✅ Enforcing the existence of a files/folders when a specific file/folder exists. For example, if `./src/Component.tsx` exists, then `./src/Component.test.tsx` and `./src/stories/Component.stories.tsx` must also exist.
✅ Reusable rules for folder structures.
✅ An option to create a separate configuration file with TypeScript support.
✅ Forcing a nested/flat structure for a given folder.
@@ -164,20 +164,18 @@ Create a **`folderStructure.mjs`** in the root of your project.
import { createFolderStructure } from "eslint-plugin-project-structure"; export const folderStructureConfig = createFolderStructure({ - structure: { - children: [ - // Allow any files in the root of your project, like package.json, eslint.config.mjs, etc. You can add rules for them separately. - // You can also add exceptions like this: "(?!folderStructure)*" - { name: "*" }, - { - name: "src", - children: [ - { name: "index.tsx" }, - { name: "components", children: [{ name: "{PascalCase}.tsx" }] }, - ], - }, - ], - }, + structure: [ + // Allow any files in the root of your project, like package.json, eslint.config.mjs, etc. You can add rules for them separately. + // You can also add exceptions like this: "(?!folderStructure)*" + { name: "*" }, + { + name: "src", + children: [ + { name: "index.tsx" }, + { name: "components", children: [{ name: "{PascalCase}.tsx" }] }, + ], + }, + ], }); ``` @@ -239,17 +237,15 @@ import { createFolderStructure } from "eslint-plugin-project-structure"; export const folderStructureConfig = createFolderStructure({ ignorePatterns: ["src/legacy/**"], - structure: { - children: [ - // Allow any files in the root of your project, like package.json, eslint.config.mjs, etc. You can add rules for them separately. - // You can also add exceptions like this: "(?!folderStructure)*" - { name: "*" }, - { - name: "src", - children: [{ ruleId: "hooks_folder" }, { ruleId: "components_folder" }], - }, - ], - }, + structure: [ + // Allow any files in the root of your project, like package.json, eslint.config.mjs, etc. You can add rules for them separately. + // You can also add exceptions like this: "(?!folderStructure)*" + { name: "*" }, + { + name: "src", + children: [{ ruleId: "hooks_folder" }, { ruleId: "components_folder" }], + }, + ], rules: { hooks_folder: { name: "hooks", @@ -441,7 +437,13 @@ In `enforceExistence`, two references are available for use: ```jsonc { "structure": { + // If root directory exists. + "enforceExistence": [ + "src", // ./src must exist. + "src/components", // ./src/components must exist. + ], "children": [ + { "name": "*" }, { "name": "src", "children": [ @@ -459,20 +461,12 @@ In `enforceExistence`, two references are available for use: }, ], }, - { - "name": "*", - // If any file exists in the root directory of the project. - "enforceExistence": [ - "src", // ./src must exist. - "src/components", // ./src/components must exist. - ], - }, ], }, } ``` -### **`structure`**: `` +### **`structure`**: ` | []` The structure of your project and its rules. @@ -490,9 +484,25 @@ The structure of your project and its rules. └── 📄 ... ``` +```jsonc +{ + "structure": [ + { "name": "libs", "children": [] }, + { "name": "src", "children": [] }, + { "name": "yourCoolFolderName", "children": [] }, + // Allow any files in the root of your project, like package.json, eslint.config.mjs, etc. You can add rules for them separately. + // You can also add exceptions like this: "(?!folderStructure)*" + { "name": "*" }, + ], +} +``` + +or + ```jsonc { "structure": { + "enforceExistence": ["src"], "children": [ { "name": "libs", "children": [] }, { "name": "src", "children": [] }, diff --git a/folderStructure.schema.json b/folderStructure.schema.json index 64171ec..01cd27a 100644 --- a/folderStructure.schema.json +++ b/folderStructure.schema.json @@ -48,7 +48,17 @@ } }, "structure": { - "$ref": "#/definitions/Rule" + "oneOf": [ + { + "$ref": "#/definitions/Rule" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/Rule" + } + } + ] }, "rules": { "type": "object", diff --git a/package.json b/package.json index 5e6bd7e..74b880c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "Igor Kowalski (Igorkowalski94)", "name": "eslint-plugin-project-structure", - "version": "2.1.17", + "version": "2.1.18", "license": "MIT", "description": "Eslint plugin with rules that will help you achieve a project structure that is scalable, consistent, and well thought out. Whether you're working alone or with a small or large team, save time by automating the reviews of key principles for a healthy project!", "keywords": [ diff --git a/src/rules/folderStructure/folderStructure.consts.ts b/src/rules/folderStructure/folderStructure.consts.ts index e10c251..870f17c 100644 --- a/src/rules/folderStructure/folderStructure.consts.ts +++ b/src/rules/folderStructure/folderStructure.consts.ts @@ -1,75 +1,6 @@ -import { JSONSchema4 } from "@typescript-eslint/utils/dist/json-schema"; - export const REFERENCES = { parentName: "{parentName}", ParentName: "{ParentName}", name: "{name}", Name: "{Name}", }; - -export const FOLDER_STRUCTURE_SCHEMA: JSONSchema4 = { - $schema: "http://json-schema.org/draft-07/schema#", - definitions: { - Rule: { - type: "object", - default: { name: "" }, - properties: { - ruleId: { - type: "string", - default: "", - }, - name: { - type: "string", - default: "", - }, - children: { - type: "array", - default: [], - items: { - $ref: "#/definitions/Rule", - }, - }, - enforceExistence: { - type: "array", - default: [], - items: { - type: "string", - }, - }, - }, - additionalProperties: false, - }, - RegexParameters: { - type: "object", - default: {}, - additionalProperties: { - type: "string", - }, - }, - }, - type: "object", - properties: { - ignorePatterns: { - type: "array", - default: [], - items: { - type: "string", - }, - }, - structure: { - $ref: "#/definitions/Rule", - }, - rules: { - type: "object", - default: {}, - additionalProperties: { - $ref: "#/definitions/Rule", - }, - }, - regexParameters: { - $ref: "#/definitions/RegexParameters", - }, - }, - required: ["structure"], - additionalProperties: false, -}; diff --git a/src/rules/folderStructure/folderStructure.types.ts b/src/rules/folderStructure/folderStructure.types.ts index 489b9a0..06713d5 100644 --- a/src/rules/folderStructure/folderStructure.types.ts +++ b/src/rules/folderStructure/folderStructure.types.ts @@ -13,7 +13,7 @@ export type RegexParameters = Record; export interface FolderStructureConfig { ignorePatterns?: string[]; - structure: Rule; + structure: Rule | Rule[]; rules?: Record>; regexParameters?: RegexParameters; } diff --git a/src/rules/folderStructure/helpers/createFolderStructure.ts b/src/rules/folderStructure/helpers/createFolderStructure.ts index eb2c3ef..f106b1a 100644 --- a/src/rules/folderStructure/helpers/createFolderStructure.ts +++ b/src/rules/folderStructure/helpers/createFolderStructure.ts @@ -7,7 +7,7 @@ import { export const createFolderStructure = < R extends Record>, >(config: { - structure: Rule; + structure: Rule | Rule[]; rules?: R; ignorePatterns?: string[]; regexParameters?: RegexParameters; diff --git a/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getPaths.ts b/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getPaths.ts index c5c8ada..6e86ee4 100644 --- a/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getPaths.ts +++ b/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getPaths.ts @@ -3,6 +3,7 @@ import path, { sep } from "path"; interface GetPathsProps { cwd: string; filename: string; + rootFolderName: string; } interface GetPathsReturn { @@ -10,11 +11,15 @@ interface GetPathsReturn { filenameWithoutCwd: string; } -export const getPaths = ({ cwd, filename }: GetPathsProps): GetPathsReturn => { +export const getPaths = ({ + cwd, + filename, + rootFolderName, +}: GetPathsProps): GetPathsReturn => { const filenameWithoutCwd = path.relative(cwd, filename).replaceAll(sep, "/"); const pathname = path - .join("structure", filenameWithoutCwd) + .join(rootFolderName, filenameWithoutCwd) .replaceAll(sep, "/"); return { diff --git a/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule.test.ts b/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule.test.ts new file mode 100644 index 0000000..059c33e --- /dev/null +++ b/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule.test.ts @@ -0,0 +1,20 @@ +import { + FolderStructureConfig, + Rule, +} from "rules/folderStructure/folderStructure.types"; +import { getRootRule } from "rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule"; + +describe("getRootRule", () => { + it.each<{ structure: FolderStructureConfig["structure"]; expected: Rule }>([ + { + structure: [{ children: [] }, { name: "index.ts" }], + expected: { children: [{ children: [] }, { name: "index.ts" }] }, + }, + { + structure: { enforceExistence: [], children: [] }, + expected: { enforceExistence: [], children: [] }, + }, + ])("Should return correct value for %o", ({ structure, expected }) => { + expect(getRootRule(structure)).toEqual(expected); + }); +}); diff --git a/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule.ts b/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule.ts new file mode 100644 index 0000000..d1381df --- /dev/null +++ b/src/rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule.ts @@ -0,0 +1,15 @@ +import { + FolderStructureConfig, + Rule, +} from "rules/folderStructure/folderStructure.types"; + +export const getRootRule = ( + structure: FolderStructureConfig["structure"], +): Rule => { + if (Array.isArray(structure)) + return { + children: structure, + }; + + return structure; +}; diff --git a/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.consts.ts b/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.consts.ts new file mode 100644 index 0000000..080508f --- /dev/null +++ b/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.consts.ts @@ -0,0 +1,78 @@ +import { JSONSchema4 } from "@typescript-eslint/utils/dist/json-schema"; + +export const FOLDER_STRUCTURE_SCHEMA: JSONSchema4 = { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { + Rule: { + type: "object", + default: { name: "" }, + properties: { + ruleId: { + type: "string", + default: "", + }, + name: { + type: "string", + default: "", + }, + children: { + type: "array", + default: [], + items: { + $ref: "#/definitions/Rule", + }, + }, + enforceExistence: { + type: "array", + default: [], + items: { + type: "string", + }, + }, + }, + additionalProperties: false, + }, + RegexParameters: { + type: "object", + default: {}, + additionalProperties: { + type: "string", + }, + }, + }, + type: "object", + properties: { + ignorePatterns: { + type: "array", + default: [], + items: { + type: "string", + }, + }, + structure: { + oneOf: [ + { + $ref: "#/definitions/Rule", + }, + { + type: "array", + items: { + $ref: "#/definitions/Rule", + }, + }, + ], + }, + rules: { + type: "object", + default: {}, + additionalProperties: { + $ref: "#/definitions/Rule", + }, + }, + regexParameters: { + $ref: "#/definitions/RegexParameters", + }, + }, + required: ["structure"], + additionalProperties: false, +}; diff --git a/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.test.ts b/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.test.ts index e86c6c8..361dad0 100644 --- a/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.test.ts +++ b/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.test.ts @@ -1,3 +1,5 @@ +import path from "path"; + import { validateConfig } from "helpers/validateConfig"; import { isIgnoredPathname } from "rules/folderStructure/helpers/validateFolderStructure/helpers/isIgnoredPathname"; @@ -25,9 +27,15 @@ describe("validateFolderStructure", () => { expect( validateFolderStructure({ - cwd: "", + cwd: path.join("C:", "rootFolderName"), config: { structure: {} }, - filename: "src/features/ComponentName.tsx", + filename: path.join( + "C:", + "rootFolderName", + "src", + "features", + "ComponentName.tsx", + ), }), ).toEqual(undefined); }); @@ -40,16 +48,22 @@ describe("validateFolderStructure", () => { (validateConfig as jest.Mock).mockImplementation(); validateFolderStructure({ - cwd: "", + cwd: path.join("C:", "rootFolderName"), config: { structure: {} }, - filename: "src/features/ComponentName.tsx", + filename: path.join( + "C:", + "rootFolderName", + "src", + "features", + "ComponentName.tsx", + ), }); expect(validatePathMock).toHaveBeenCalledWith({ - pathname: "structure/src/features/ComponentName.tsx", + pathname: "rootFolderName/src/features/ComponentName.tsx", filenameWithoutCwd: "src/features/ComponentName.tsx", - cwd: "", - parentName: "structure", + cwd: path.join("C:", "rootFolderName"), + parentName: "rootFolderName", rule: {}, config: { structure: {} }, }); diff --git a/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.ts b/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.ts index 8507797..f704f9e 100644 --- a/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.ts +++ b/src/rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.ts @@ -1,9 +1,12 @@ +import { sep } from "path"; + import { validateConfig } from "helpers/validateConfig"; -import { FOLDER_STRUCTURE_SCHEMA } from "rules/folderStructure/folderStructure.consts"; import { FolderStructureConfig } from "rules/folderStructure/folderStructure.types"; import { getPaths } from "rules/folderStructure/helpers/validateFolderStructure/helpers/getPaths"; +import { getRootRule } from "rules/folderStructure/helpers/validateFolderStructure/helpers/getRootRule"; import { isIgnoredPathname } from "rules/folderStructure/helpers/validateFolderStructure/helpers/isIgnoredPathname"; +import { FOLDER_STRUCTURE_SCHEMA } from "rules/folderStructure/helpers/validateFolderStructure/validateFolderStructure.consts"; import { validatePath } from "rules/folderStructure/helpers/validatePath/validatePath"; interface ValidateFolderStructureProps { @@ -21,7 +24,14 @@ export const validateFolderStructure = ({ const { structure, ignorePatterns } = config; - const { filenameWithoutCwd, pathname } = getPaths({ cwd, filename }); + const rootRule = getRootRule(structure); + const rootFolderName = cwd.split(sep).reverse()[0]; + + const { filenameWithoutCwd, pathname } = getPaths({ + cwd, + filename, + rootFolderName, + }); if (isIgnoredPathname({ pathname: filenameWithoutCwd, ignorePatterns })) return; @@ -30,8 +40,8 @@ export const validateFolderStructure = ({ pathname, filenameWithoutCwd, cwd, - parentName: "structure", - rule: structure, + parentName: rootFolderName, + rule: rootRule, config, }); };