├── .eslintrc.json ├── .github ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FAQ.md ├── LICENSE ├── README.md ├── index.js ├── index.legacy.js ├── package.json ├── packages ├── cli │ ├── README.md │ ├── bin.js │ ├── package.json │ ├── rollup.conf.js │ ├── src │ │ ├── index.js │ │ ├── log.js │ │ ├── packages.js │ │ ├── run.js │ │ └── ui.js │ ├── tests │ │ └── index.test.js │ └── yarn.lock └── processor │ ├── README.md │ ├── index.js │ ├── messages.js │ └── package.json ├── rules ├── import-order │ ├── README.md │ ├── experimental.js │ ├── experimental.test.js │ ├── index.js │ └── index.test.js ├── integration.test.js ├── integration.ts.test.js ├── layers-slices │ ├── README.md │ ├── index.js │ ├── layers.test.js │ └── slices.test.js └── public-api │ ├── README.md │ ├── index.js │ ├── index.test.js │ ├── lite.js │ ├── lite.test.js │ └── segment-public-api.test.js ├── test ├── config.test.js └── lint.test.js ├── utils ├── config │ ├── index.js │ └── mock-resolver.js ├── index.js ├── layers.js └── types.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@eslint-kit/patch", 4 | "@eslint-kit/base", 5 | "@eslint-kit/prettier" 6 | ], 7 | "rules": { 8 | "no-undef": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## References 4 | 5 | ## Checklist 6 | - [ ] Description added 7 | - [ ] Self-reviewed 8 | - [ ] CI pass 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test current build 2 | on: [push] 3 | jobs: 4 | test_build: 5 | runs-on: ubuntu-latest 6 | timeout-minutes: 3 7 | strategy: 8 | matrix: 9 | node-version: [12.x] 10 | steps: 11 | - name: Checkout Repository 12 | uses: actions/checkout@v1 13 | 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | 19 | - name: Install deps and run tests 20 | run: | 21 | npm install 22 | npm run test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | node_modules/ 4 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.test.* 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | dist/** 3 | build/** 4 | public/ 5 | .github 6 | .workflows 7 | *.md 8 | *.mdx 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | semi: true, 4 | singleQuote: false, 5 | tabWidth: 4, 6 | quoteProps: "consistent", 7 | endOfLine: "lf", 8 | importOrder: ["^[./]"], 9 | trailingComma: "all", 10 | arrowParens: "always", 11 | }; 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at martis.azin@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thanks for taking the time to contribute! :+1: 4 | 5 | ## How Can I Contribute? 6 | 7 | [issues]: https://github.com/feature-sliced/eslint-config/issues 8 | [issues-new]: https://github.com/feature-sliced/eslint-config/issues/new 9 | [pr]: https://github.com/feature-sliced/eslint-config/pulls 10 | [pr-new]: https://github.com/feature-sliced/eslint-config/compare 11 | [disc]: https://github.com/feature-sliced/eslint-config/discussions/7 12 | 13 | 14 | 15 | - 📢 [Give feedback][disc] 16 | > We'll glad to get any feedback from you! 17 | - 💡 [Report bugs, suggest improvements][issues-new] 18 | > If something specific doesn't work well for you or can be done better, please let us know! 19 | - 💬 Estimate & discuss [issues][issues] 20 | > Share your opinion, evaluate given problem context from author 21 | - 🔩 Repeat difficult [issues][issues] 22 | > Some issues hard to repeat 23 | - 🛡️ Review [pull requests][pr] 24 | > Share your opinion and help us with others' suggestions 25 | - ⚒️ Suggest [your own pull-requests!][pr-new] 26 | > Reinforce project by your code solution 27 | 28 | ## Workflow 29 | 1. Fork repository 30 | 2. Add your changes 31 | - Ensure that **commits messages conforms** to [Conventional Commits](https://www.conventionalcommits.org) spec. 32 | - Ensure that **all tests are passing** 33 | ```sh 34 | $ npm run test # mocha will be started 35 | ``` 36 | 3. Propose your pull-request by *your forked branch* and specify related issues, if they are exist 37 | - Ensure that **[CI](https://github.com/feature-sliced/eslint-config/actions)** is passing for your PR 38 | > Our goal - to dev good-quality solution in every sense 39 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ### How to test eslint-config locally without publishing? 4 | 5 | See [npm-link](https://docs.npmjs.com/cli/v8/commands/npm-link) 6 | 7 | ### Is there any existing eslint-config drafts? 8 | 9 | See [here](https://gist.github.com/azinit/4cb940a1d4a3e05ef47e15aa18a9ecc5) 10 | 11 | ### Where I can find actual rules? 12 | 13 | See [here](../index.js) 14 | 15 | ### How to publish last added rules? 16 | 17 | You only need to send [pull-request](https://github.com/feature-sliced/eslint-config/pulls) 18 | 19 | Publishing is realized only by FeatureSliced core-team 🍰 20 | 21 | ### Where I can suggest new boundaries? 22 | 23 | Create [new issue](https://github.com/feature-sliced/eslint-config/issues/new) 24 | 25 | ### Are there plans about migrating to eslint-plugin? 26 | 27 | At the moment we wanna to use existing solutions for "MVP" linting of FeatureSliced best practices 28 | 29 | Later, if we'll make sure that eslint-config is not enough - then we'll migrate to eslint-plugin with compatibility and migration guides of eslint-config users 30 | 31 | > Compatibility First 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 feature-sliced 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [@feature-sliced/eslint-config](https://www.npmjs.com/package/@feature-sliced/eslint-config) 2 | 3 | > `WIP:` At the moment at beta-testing - [use carefully](https://github.com/feature-sliced/eslint-config/discussions/75) 4 | 5 | [npm]: https://www.npmjs.com/package/@feature-sliced/eslint-config 6 | 7 | [![npm](https://img.shields.io/npm/v/@feature-sliced/eslint-config?style=flat-square)][npm] 8 | [![npm](https://img.shields.io/npm/dw/@feature-sliced/eslint-config?style=flat-square)][npm] 9 | [![npm bundle size](https://img.shields.io/bundlephobia/min/@feature-sliced/eslint-config?style=flat-square)][npm] 10 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/feature-sliced/eslint-config/Test%20current%20build?label=tests&style=flat-square)](https://github.com/feature-sliced/eslint-config/actions) 11 | 12 | 13 | 14 | Linting of [FeatureSliced](https://github.com/feature-sliced/documentation) concepts *by existing eslint-plugins* 15 | 16 | - Control [**Isolation**](https://feature-sliced.design/docs/concepts/low-coupling) & [**Decomposition**](https://feature-sliced.design/docs/concepts/app-splitting) 17 | - Control [**Public API**](https://feature-sliced.design/docs/concepts/public-api) 18 | - Control [**Layers & Scopes**](https://feature-sliced.design/docs/reference/layers) 19 | - Control [**Naming**](https://feature-sliced.design/docs/concepts/naming-adaptability) 20 | 21 | 30 | 31 | ## Rules 32 | 33 | Each rule has its own test cases and customization aspects 34 | 35 | - [`import-order`](./rules/import-order) 36 | - [`public-api`](./rules/public-api) 37 | - [`layers-slices`](./rules/layers-slices) 38 | 39 | ## Get Started 40 | 41 | 1. You'll first need to install [ESLint](http://eslint.org): 42 | 43 | ```sh 44 | $ npm install -D eslint 45 | # or by yarn 46 | $ yarn add -D eslint 47 | $ or by pnpm 48 | $ pnpm add -D eslint 49 | ``` 50 | 51 | 2. Next, install `@feature-sliced/eslint-config` and dependencies: 52 | 53 | ```sh 54 | $ npm install -D @feature-sliced/eslint-config eslint-plugin-import eslint-plugin-boundaries 55 | # or by yarn 56 | $ yarn add -D @feature-sliced/eslint-config eslint-plugin-import eslint-plugin-boundaries 57 | # or by pnpm 58 | $ pnpm add -D @feature-sliced/eslint-config eslint-plugin-import eslint-plugin-boundaries 59 | ``` 60 | 61 | 3. Add config to the `extends` section of your `.eslintrc` configuration file (for **recommended** rules). You can omit the `eslint-config` postfix: 62 | 63 | ```json 64 | { 65 | "extends": ["@feature-sliced"] 66 | } 67 | ``` 68 | 69 | 4. `TYPESCRIPT-ONLY:` Also setup TS-parser and TS-plugin [(why?)](https://github.com/javierbrea/eslint-plugin-boundaries#usage-with-typescript) 70 |
71 | Details 72 | 73 | **Install dependencies:** 74 | 75 | ```sh 76 | $ npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-import-resolver-typescript 77 | # or by yarn 78 | $ yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-import-resolver-typescript 79 | ``` 80 | 81 | **Configure `@typescript-eslint/parser` as parser and setup the `eslint-import-resolver-typescript` resolver in the `.eslintrc` config file:** 82 | 83 | ```json 84 | { 85 | "parser": "@typescript-eslint/parser", 86 | "settings": { 87 | "import/resolver": { 88 | "typescript": { 89 | "alwaysTryTypes": true 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 |
97 | 98 | ## Usage 99 | 100 | - Support general **aliases** 101 | 102 | ```js 103 | import { Input } from "~/shared/ui/input"; 104 | import { Input } from "@/shared/ui/input"; 105 | import { Input } from "@shared/ui/input"; 106 | import { Input } from "$shared/ui/input"; 107 | // But not - import { Input } from "$UIKit/input"; 108 | ``` 109 | 110 | - Support **relative** and **absolute** imports (but look at [recommendations](https://github.com/feature-sliced/eslint-config/issues/29)) 111 | 112 | ```js 113 | import { ... } from "entities/foo"; // absolute imports 114 | import { ... } from "@/entities/foo"; // aliased imports 115 | import { ... } from "../entities/foo"; // relative imports 116 | ``` 117 | 118 | - **Case**-agnostic 119 | 120 | ```js 121 | import { ... } from "entities/user-post"; // Support kebab-case (recommended) 122 | import { ... } from "entities/UserPost"; // Support PascalCase 123 | import { ... } from "entities/userPost"; // Support camelCase 124 | import { ... } from "entities/user_post"; // Support snake_case 125 | ``` 126 | 127 | - For exceptional cases, support ⚠️**DANGEROUS-mode**⚠️ (see more for [specific rule](#rules)) 128 | 129 | ## Customization 130 | 131 | 1. You can *partially use* the rules 132 | 133 | > **WARN:** Don't use main config (`"@feature-sliced"`) in customization to avoid rules conflicts. 134 | 135 | ```js 136 | "extends": [ 137 | "@feature-sliced/eslint-config/rules/import-order", 138 | "@feature-sliced/eslint-config/rules/public-api", 139 | "@feature-sliced/eslint-config/rules/layers-slices", 140 | ] 141 | ``` 142 | 143 | 1. You can use *alternative experimental rules* 144 | - Use [`import-order/experimental`](./rules/import-order#Experimental) for formatting with spaces between groups and reversed order of layers [(why?)](https://github.com/feature-sliced/eslint-config/issues/85) 145 | 146 | ```js 147 | "extends": [ 148 | // ... Other rules or config 149 | "@feature-sliced/eslint-config/rules/import-order/experimental", 150 | ] 151 | ``` 152 | 153 | - Use [`public-api/lite`](./rules/public-api#Lite) for less strict PublicAPI boundaries [(why?)](https://github.com/feature-sliced/eslint-config/issues/90) 154 | 155 | ```js 156 | "extends": [ 157 | // ... Other rules or config 158 | "@feature-sliced/eslint-config/rules/public-api/lite", 159 | ] 160 | ``` 161 | 162 | 1. You can use *warnings* instead of *errors* for specific rules 163 | 164 | ```js 165 | "rules": { 166 | // feature-sliced/import-order 167 | "import/order": "warn" // ~ 1, 168 | // feature-sliced/public-api 169 | "import/no-internal-modules": "warn" // ~ 1, 170 | // feature-sliced/layers-slices 171 | "boundaries/element-types": "warn" // ~ 1, 172 | } 173 | ``` 174 | 175 | 1. You can use *[advanced FSD-specific messages processing](https://www.npmjs.com/package/@feature-sliced/eslint-plugin-messages)* 176 | 177 | ```diff 178 | # (feature-sliced/public-api) 179 | - 'Reaching to "features/search/ui" is not allowed.' 180 | + 'Violated usage of modules Public API | https://git.io/Jymjf' 181 | ``` 182 | 183 | ## See also 184 | 185 | - [FAQ](./FAQ.md) 186 | - [Releases & Changelog](https://github.com/feature-sliced/eslint-config/releases) 187 | - [**How can I help?**](./CONTRIBUTING.md) 188 | - ⭐ Rate us on GitHub 189 | - 💫 **Any assistance is important** - from *feedback to participation in the development of the methodology*! 190 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | parserOptions: { 5 | "ecmaVersion": "2015", 6 | "sourceType": "module", 7 | }, 8 | extends: [ 9 | path.resolve(__dirname, "./rules/public-api"), 10 | path.resolve(__dirname, "./rules/layers-slices"), 11 | path.resolve(__dirname, "./rules/import-order") 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /index.legacy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DO NOT MODIFY THIS FILE 3 | * THERE ARE ONLY OLD RULES THAT SHOULD BE ACTUALIZED 4 | * @see https://github.com/feature-sliced/eslint-config/issues/17 5 | */ 6 | 7 | // Allowed paths for public API 8 | const PUBLIC_PATHS = [ 9 | "app", 10 | "pages", 11 | "features", 12 | "shared", 13 | "shared/**", 14 | "models", 15 | ]; 16 | // Private imports are prohibited, use public imports instead 17 | const PRIVATE_PATHS = [ 18 | "app/**", 19 | "pages/**", 20 | "features/**", 21 | "shared/*/**", 22 | ]; 23 | // Prefer absolute imports instead of relatives (for root modules) 24 | const RELATIVE_PATHS = [ 25 | "../**/app", 26 | "../**/pages", 27 | "../**/features", 28 | "../**/shared", 29 | "../**/models", 30 | ]; 31 | 32 | module.exports = { 33 | parserOptions: { 34 | ecmaVersion: "2015", 35 | sourceType: "module", 36 | }, 37 | plugins: [ 38 | "import", 39 | ], 40 | rules: { 41 | "import/first": 2, 42 | "import/no-unresolved": 0, // experimental 43 | "import/order": [ 44 | 2, 45 | { 46 | pathGroups: PUBLIC_PATHS.map( 47 | (pattern) => ({ 48 | pattern, 49 | group: "internal", 50 | position: "after", 51 | }), 52 | ), 53 | pathGroupsExcludedImportTypes: ["builtin"], 54 | groups: ["builtin", "external", "internal", "parent", "sibling", "index"], 55 | }, 56 | ], 57 | // TODO: with messages (https://github.com/feature-sliced/eslint-config/issues/3) 58 | "no-restricted-imports": [ 59 | 2, 60 | { 61 | patterns: [...PRIVATE_PATHS, ...RELATIVE_PATHS] 62 | } 63 | ], 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feature-sliced/eslint-config", 3 | "version": "0.1.0-beta.6", 4 | "description": "🍰 Lint feature-sliced concepts by existing eslint plugins", 5 | "main": "index.js", 6 | "files": [ 7 | "rules/**/experimental.js", 8 | "rules/**/lite.js", 9 | "rules/**/index.js", 10 | "utils/**", 11 | "index.js" 12 | ], 13 | "repository": "https://github.com/feature-sliced/eslint-config.git", 14 | "author": "FeatureSliced core-team", 15 | "license": "MIT", 16 | "keywords": [ 17 | "eslint", 18 | "eslintconfig", 19 | "eslint-config", 20 | "feature-sliced", 21 | "feature-slices", 22 | "feature-driven", 23 | "feature-based" 24 | ], 25 | "scripts": { 26 | "publish:prepatch": "npm version prepatch && npm publish", 27 | "publish:patch": "npm version patch && npm publish", 28 | "publish:minor": "npm version minor && npm publish", 29 | "publish:major": "npm version major && npm publish", 30 | "prettier:fix": "prettier --write **/*.js", 31 | "clean": "git clean -fxd", 32 | "test": "mocha \"*(test|rules)/**/*.test.js\"" 33 | }, 34 | "peerDependencies": { 35 | "eslint-plugin-boundaries": ">=2", 36 | "eslint-plugin-import": ">=2" 37 | }, 38 | "publishConfig": { 39 | "access": "public" 40 | }, 41 | "devDependencies": { 42 | "@eslint-kit/eslint-config-base": "^5.0.2", 43 | "@eslint-kit/eslint-config-patch": "^1.0.0", 44 | "@eslint-kit/eslint-config-prettier": "^4.0.0", 45 | "@typescript-eslint/parser": "^5.6.0", 46 | "eslint": "7.10.0", 47 | "eslint-import-resolver-node": "^0.3.6", 48 | "eslint-plugin-boundaries": "^2.6.0", 49 | "eslint-plugin-import": "^2.25.3", 50 | "mocha": "^8.2.1", 51 | "prettier": "2.3.0", 52 | "typescript": "^4.5.3" 53 | }, 54 | "dependencies": {} 55 | } 56 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # [@feature-sliced/eslint-config-cli](https://www.npmjs.com/package/@feature-sliced/config-cli) 2 | 3 | > `WIP:` At the moment at beta-testing - [use carefully](https://github.com/feature-sliced/eslint-config/discussions/75) 4 | 5 | 6 | 7 | CLI for more comfortable usage of [@feature-sliced/eslint-config](https://www.npmjs.com/package/@feature-sliced/eslint-config) 8 | 9 | - Quick bootstraping with dependencies installing 10 | - *TBA: Config customization* 11 | 12 | ## How it works? 13 | 14 | CLI stores the dependencies necessary for @feature-sliced/eslint-config to work. 15 | 16 | 1. **At start, it checks whether the project is a Typescript project** 17 | The package file is parsed.the user's json, and if the @types/* packages or the typescript package are found in it, the project is recognized as a TS project. 18 | 19 | 2. **Next, the ui-prompt is launched, which confirms the installation of TS packages from the user, and the installation in general.** 20 | With the consent of the user, the installation process is started, the presence of installed packages is checked by the user, in case of presence, the package is skipped (filtered), if started with force, all packages will be forcibly installed. 21 | -------------------------------------------------------------------------------- /packages/cli/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("./dist/cli.js"); 3 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feature-sliced/eslint-cli", 3 | "version": "0.0.0-concept.3", 4 | "description": "🍰 CLI for eslint-config convenient usage", 5 | "keywords": [ 6 | "eslint", 7 | "eslintconfig", 8 | "eslint-config", 9 | "feature-sliced", 10 | "feature-slices", 11 | "feature-driven", 12 | "feature-based" 13 | ], 14 | "main": "dist/cli.js", 15 | "bin": { 16 | "@feature-sliced/eslint-cli": "bin.js" 17 | }, 18 | "scripts": { 19 | "build": "rollup -c rollup.conf.js", 20 | "test": "DEBUG=2 mocha tests/*.test.js", 21 | "cli:run": "node src/index.js", 22 | "cli:dev": "DEBUG=1 node src/index.js" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "repository": "https://github.com/feature-sliced/eslint-config.git", 28 | "author": "FeatureSliced core-team", 29 | "license": "MIT", 30 | "dependencies": { 31 | }, 32 | "devDependencies": { 33 | "@rollup/plugin-commonjs": "^21.0.1", 34 | "@rollup/plugin-json": "^4.1.0", 35 | "@rollup/plugin-node-resolve": "^13.1.3", 36 | "clear": "^0.1.0", 37 | "lodash": "^4.17.21", 38 | "mocha": "^9.2.1", 39 | "picocolors": "^1.0.0", 40 | "prompts": "^2.4.2", 41 | "rollup": "^2.67.3", 42 | "rollup-plugin-terser": "^7.0.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/rollup.conf.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import json from "@rollup/plugin-json"; 3 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 4 | import { terser } from "rollup-plugin-terser"; 5 | 6 | export default { 7 | input: "src/index.js", 8 | output: { 9 | file: "dist/cli.js", 10 | format: "cjs", 11 | }, 12 | plugins: [nodeResolve({ include: ["node_modules/**"] }), commonjs(), json(), terser()], 13 | }; 14 | -------------------------------------------------------------------------------- /packages/cli/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const _ = require("lodash"); 3 | const { installCmdBuilder, installDependencies } = require("./run"); 4 | const { 5 | getPkgManger, 6 | depsPackages, 7 | basicPackages, 8 | typescriptPackages, 9 | filterInstalledDeps, 10 | getUserDeps, 11 | isTypeScriptProject, 12 | } = require("./packages"); 13 | const { ui } = require("./ui"); 14 | const { log } = require("./log"); 15 | 16 | const packageJsonRaw = fs.readFileSync("package.json"); 17 | const packageInfo = JSON.parse(packageJsonRaw); 18 | const userDeps = getUserDeps(packageInfo); 19 | 20 | function bootstrap({ withTs, force = false }) { 21 | if (process.env.DEBUG) console.info("Bootstraping with ts/force:", withTs, force); 22 | 23 | log.info("@feature-sliced/eslint-config/cli"); 24 | 25 | const userPkgManager = getPkgManger(); 26 | if (!userPkgManager) { 27 | return; 28 | } 29 | log.info(`Found ${userPkgManager}. Start install missing dependencies.`); 30 | 31 | const runInstall = installCmdBuilder(userPkgManager); 32 | const installDeps = force ? depsPackages : filterInstalledDeps(depsPackages, userDeps); 33 | let tsDeps = {}; 34 | 35 | if (withTs) { 36 | tsDeps = force ? typescriptPackages : filterInstalledDeps(typescriptPackages, userDeps); 37 | } 38 | 39 | installDependencies(runInstall, _.merge(installDeps, basicPackages, tsDeps)); 40 | 41 | log.info(`Done.`); 42 | } 43 | 44 | ui(bootstrap, isTypeScriptProject(userDeps)); 45 | -------------------------------------------------------------------------------- /packages/cli/src/log.js: -------------------------------------------------------------------------------- 1 | const pc = require("picocolors"); 2 | 3 | const log = { 4 | error: (text) => console.error(pc.red(text)), 5 | warn: (text) => console.warn(pc.yellow(text)), 6 | info: (text) => console.info(pc.green(text)), 7 | }; 8 | 9 | module.exports = { log }; 10 | -------------------------------------------------------------------------------- /packages/cli/src/packages.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { log } = require("./log"); 4 | const _ = require("lodash"); 5 | 6 | const basicPackages = { 7 | "@feature-sliced/eslint-config": "latest", 8 | }; 9 | 10 | const depsPackages = { 11 | "eslint-plugin-boundaries": "^2.8.0", 12 | "eslint-plugin-import": "^2.25.4", 13 | }; 14 | 15 | const typescriptPackages = { 16 | "@typescript-eslint/eslint-plugin": "latest", 17 | "@typescript-eslint/parser": "latest", 18 | "eslint-import-resolver-typescript": "latest", 19 | }; 20 | 21 | const PkgMangers = { 22 | npm: { lock: "package-lock.json", install: "install" }, 23 | yarn: { lock: "yarn.lock", install: "add" }, 24 | pnpm: { lock: "pnpm-lock.yaml", install: "install" }, 25 | }; 26 | 27 | function isTypeScriptProject(userDeps) { 28 | if (process.env.DEBUG) console.info("Detected user dependencies:", userDeps); 29 | for (const dep in userDeps) { 30 | if (process.env.DEBUG) console.info("processed:", dep); 31 | if (dep.includes("@types/") || dep.includes("typescript")) { 32 | if (process.env.DEBUG) console.info("Detected TS on:", dep); 33 | return true; 34 | } 35 | } 36 | return false; 37 | } 38 | 39 | function getUserDeps(packageInfo) { 40 | return _.merge(packageInfo.dependencies, packageInfo.devDependencies); 41 | } 42 | 43 | function filterInstalledDeps(installDeps, existDeps) { 44 | const exist = Object.keys(existDeps); 45 | return Object.keys(installDeps).reduce( 46 | (result, dep) => (exist.includes(dep) ? result : { ...result, [dep]: installDeps[dep] }), 47 | {}, 48 | ); 49 | } 50 | 51 | function getPkgManger() { 52 | const pkgManagersNames = Object.keys(PkgMangers); 53 | 54 | const selectedPkgManagers = pkgManagersNames.reduce((result, pkgManager) => { 55 | const pkgManagerPath = path.resolve(PkgMangers[pkgManager].lock); 56 | 57 | try { 58 | const exist = fs.existsSync(pkgManagerPath); 59 | if (exist) return [...result, pkgManager]; 60 | } catch (error) {} 61 | 62 | return result; 63 | }, []); 64 | 65 | if (selectedPkgManagers.length === 0) { 66 | log.error("Something wrong! No one package manager found in project! Stopped!"); 67 | return null; 68 | } 69 | 70 | if (selectedPkgManagers.length > 1) { 71 | log.error("Something wrong! Find more then one package manager in project! Stopped!"); 72 | return null; 73 | } 74 | 75 | return selectedPkgManagers[0]; 76 | } 77 | 78 | function withPkgManager(cmdExecutor, pkgManager) { 79 | return function () { 80 | cmdExecutor.call(null, ...arguments, pkgManager); 81 | }; 82 | } 83 | 84 | module.exports = { 85 | withPkgManager, 86 | getPkgManger, 87 | PkgMangers, 88 | basicPackages, 89 | depsPackages, 90 | typescriptPackages, 91 | getUserDeps, 92 | filterInstalledDeps, 93 | isTypeScriptProject, 94 | }; 95 | -------------------------------------------------------------------------------- /packages/cli/src/run.js: -------------------------------------------------------------------------------- 1 | const { log } = require("./log"); 2 | const { spawnSync } = require("child_process"); 3 | const { PkgMangers, withPkgManager } = require("./packages"); 4 | 5 | function runCmdFactory(cmd, executor) { 6 | return function (cmdArgs) { 7 | executor.call(null, [cmd, cmdArgs]); 8 | }; 9 | } 10 | 11 | function exec(cmd, pkgManager = null) { 12 | if (!pkgManager) { 13 | log.error("No one package manager found in cmd scope!"); 14 | return; 15 | } 16 | 17 | log.info(`> install ${cmd.slice(-1)}`); 18 | if (process.env.DEBUG) return; 19 | 20 | try { 21 | const spawnResultBuffer = spawnSync(pkgManager, [...cmd], { 22 | shell: true, 23 | stdio: ["ignore", process.stdout, process.stderr], 24 | }); 25 | } catch (error) { 26 | console.error(error); 27 | } 28 | } 29 | 30 | function installCmdBuilder(userPkgManager) { 31 | const installCmd = PkgMangers[userPkgManager].install; 32 | const userExec = withPkgManager(exec, userPkgManager); 33 | return runCmdFactory(installCmd, userExec); 34 | } 35 | 36 | function installDependencies(installFn, dependencies, dev = true) { 37 | const depsString = Object.keys(dependencies).reduce((result, dep) => { 38 | const version = dependencies[dep] && `@${dependencies[dep]}`; 39 | return `${result} "${dep + version}"`; 40 | }, ""); 41 | 42 | const installArgs = `${dev && "-D"}${depsString}`; 43 | 44 | return installFn(installArgs); 45 | } 46 | 47 | module.exports = { exec, installDependencies, installCmdBuilder }; 48 | -------------------------------------------------------------------------------- /packages/cli/src/ui.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts"); 2 | const { log } = require("./log"); 3 | 4 | const HELLO_MESSAGE = "Welcome to @feature-sliced/eslint-config installer."; 5 | const INSTALL_MESSAGE = "Run installation?"; 6 | const TYPESCRIPT_MESSAGE = 7 | "Typescript detected in your project. Install additionally typescript dependencies?"; 8 | 9 | const questions = [ 10 | { 11 | type: "confirm", 12 | name: "install", 13 | message: INSTALL_MESSAGE, 14 | }, 15 | ]; 16 | 17 | const tsQuestions = [ 18 | { 19 | type: "confirm", 20 | name: "typescript", 21 | message: TYPESCRIPT_MESSAGE, 22 | }, 23 | ]; 24 | 25 | async function ui(install, typescript) { 26 | const usedQuestions = typescript ? [...tsQuestions, ...questions] : questions; 27 | 28 | log.info(HELLO_MESSAGE); 29 | 30 | if (process.env.DEBUG === "2") { 31 | install({ withTs: true, force: true }); 32 | return; 33 | } 34 | 35 | const answers = await prompts(usedQuestions); 36 | if (answers.install) { 37 | install({ withTs: answers.typescript }); 38 | } 39 | } 40 | 41 | module.exports = { ui }; 42 | -------------------------------------------------------------------------------- /packages/cli/tests/index.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | 3 | it("Should run with normall output", async () => { 4 | const run = new Promise((resolve, reject) => { 5 | const spawn = require("child_process").spawn; 6 | const command = spawn("node", ["./src/index.js"]); 7 | let result = ""; 8 | command.stdout.on("data", (data) => { 9 | result += data.toString(); 10 | }); 11 | command.on("close", (_) => { 12 | resolve(result); 13 | }); 14 | command.on("error", (error) => { 15 | reject(error); 16 | }); 17 | }); 18 | 19 | await assert.doesNotReject(run); 20 | 21 | const result = await run; 22 | 23 | assert.strictEqual(result.includes("Done."), true); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/cli/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.10.4": 6 | version "7.16.7" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" 8 | integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== 9 | dependencies: 10 | "@babel/highlight" "^7.16.7" 11 | 12 | "@babel/helper-validator-identifier@^7.16.7": 13 | version "7.16.7" 14 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" 15 | integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== 16 | 17 | "@babel/highlight@^7.16.7": 18 | version "7.16.10" 19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" 20 | integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== 21 | dependencies: 22 | "@babel/helper-validator-identifier" "^7.16.7" 23 | chalk "^2.0.0" 24 | js-tokens "^4.0.0" 25 | 26 | "@rollup/plugin-commonjs@^21.0.1": 27 | version "21.0.1" 28 | resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz#1e57c81ae1518e4df0954d681c642e7d94588fee" 29 | integrity sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg== 30 | dependencies: 31 | "@rollup/pluginutils" "^3.1.0" 32 | commondir "^1.0.1" 33 | estree-walker "^2.0.1" 34 | glob "^7.1.6" 35 | is-reference "^1.2.1" 36 | magic-string "^0.25.7" 37 | resolve "^1.17.0" 38 | 39 | "@rollup/plugin-json@^4.1.0": 40 | version "4.1.0" 41 | resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" 42 | integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== 43 | dependencies: 44 | "@rollup/pluginutils" "^3.0.8" 45 | 46 | "@rollup/plugin-node-resolve@^13.1.3": 47 | version "13.1.3" 48 | resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.1.3.tgz#2ed277fb3ad98745424c1d2ba152484508a92d79" 49 | integrity sha512-BdxNk+LtmElRo5d06MGY4zoepyrXX1tkzX2hrnPEZ53k78GuOMWLqmJDGIIOPwVRIFZrLQOo+Yr6KtCuLIA0AQ== 50 | dependencies: 51 | "@rollup/pluginutils" "^3.1.0" 52 | "@types/resolve" "1.17.1" 53 | builtin-modules "^3.1.0" 54 | deepmerge "^4.2.2" 55 | is-module "^1.0.0" 56 | resolve "^1.19.0" 57 | 58 | "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": 59 | version "3.1.0" 60 | resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" 61 | integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== 62 | dependencies: 63 | "@types/estree" "0.0.39" 64 | estree-walker "^1.0.1" 65 | picomatch "^2.2.2" 66 | 67 | "@types/estree@*": 68 | version "0.0.51" 69 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" 70 | integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== 71 | 72 | "@types/estree@0.0.39": 73 | version "0.0.39" 74 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" 75 | integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== 76 | 77 | "@types/node@*": 78 | version "17.0.18" 79 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074" 80 | integrity sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA== 81 | 82 | "@types/resolve@1.17.1": 83 | version "1.17.1" 84 | resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" 85 | integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== 86 | dependencies: 87 | "@types/node" "*" 88 | 89 | "@ungap/promise-all-settled@1.1.2": 90 | version "1.1.2" 91 | resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" 92 | integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== 93 | 94 | ansi-colors@4.1.1: 95 | version "4.1.1" 96 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" 97 | integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== 98 | 99 | ansi-regex@^5.0.1: 100 | version "5.0.1" 101 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 102 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 103 | 104 | ansi-styles@^3.2.1: 105 | version "3.2.1" 106 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 107 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 108 | dependencies: 109 | color-convert "^1.9.0" 110 | 111 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 112 | version "4.3.0" 113 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 114 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 115 | dependencies: 116 | color-convert "^2.0.1" 117 | 118 | anymatch@~3.1.2: 119 | version "3.1.2" 120 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 121 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 122 | dependencies: 123 | normalize-path "^3.0.0" 124 | picomatch "^2.0.4" 125 | 126 | argparse@^2.0.1: 127 | version "2.0.1" 128 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 129 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 130 | 131 | balanced-match@^1.0.0: 132 | version "1.0.2" 133 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 134 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 135 | 136 | binary-extensions@^2.0.0: 137 | version "2.2.0" 138 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 139 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 140 | 141 | brace-expansion@^1.1.7: 142 | version "1.1.11" 143 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 144 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 145 | dependencies: 146 | balanced-match "^1.0.0" 147 | concat-map "0.0.1" 148 | 149 | braces@~3.0.2: 150 | version "3.0.2" 151 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 152 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 153 | dependencies: 154 | fill-range "^7.0.1" 155 | 156 | browser-stdout@1.3.1: 157 | version "1.3.1" 158 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 159 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 160 | 161 | buffer-from@^1.0.0: 162 | version "1.1.2" 163 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 164 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 165 | 166 | builtin-modules@^3.1.0: 167 | version "3.2.0" 168 | resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" 169 | integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== 170 | 171 | camelcase@^6.0.0: 172 | version "6.3.0" 173 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" 174 | integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== 175 | 176 | chalk@^2.0.0: 177 | version "2.4.2" 178 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 179 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 180 | dependencies: 181 | ansi-styles "^3.2.1" 182 | escape-string-regexp "^1.0.5" 183 | supports-color "^5.3.0" 184 | 185 | chalk@^4.1.0: 186 | version "4.1.2" 187 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 188 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 189 | dependencies: 190 | ansi-styles "^4.1.0" 191 | supports-color "^7.1.0" 192 | 193 | chokidar@3.5.3: 194 | version "3.5.3" 195 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 196 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 197 | dependencies: 198 | anymatch "~3.1.2" 199 | braces "~3.0.2" 200 | glob-parent "~5.1.2" 201 | is-binary-path "~2.1.0" 202 | is-glob "~4.0.1" 203 | normalize-path "~3.0.0" 204 | readdirp "~3.6.0" 205 | optionalDependencies: 206 | fsevents "~2.3.2" 207 | 208 | clear@^0.1.0: 209 | version "0.1.0" 210 | resolved "https://registry.yarnpkg.com/clear/-/clear-0.1.0.tgz#b81b1e03437a716984fd7ac97c87d73bdfe7048a" 211 | integrity sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw== 212 | 213 | cliui@^7.0.2: 214 | version "7.0.4" 215 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" 216 | integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== 217 | dependencies: 218 | string-width "^4.2.0" 219 | strip-ansi "^6.0.0" 220 | wrap-ansi "^7.0.0" 221 | 222 | color-convert@^1.9.0: 223 | version "1.9.3" 224 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 225 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 226 | dependencies: 227 | color-name "1.1.3" 228 | 229 | color-convert@^2.0.1: 230 | version "2.0.1" 231 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 232 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 233 | dependencies: 234 | color-name "~1.1.4" 235 | 236 | color-name@1.1.3: 237 | version "1.1.3" 238 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 239 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 240 | 241 | color-name@~1.1.4: 242 | version "1.1.4" 243 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 244 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 245 | 246 | commander@^2.20.0: 247 | version "2.20.3" 248 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 249 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 250 | 251 | commondir@^1.0.1: 252 | version "1.0.1" 253 | resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" 254 | integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= 255 | 256 | concat-map@0.0.1: 257 | version "0.0.1" 258 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 259 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 260 | 261 | debug@4.3.3: 262 | version "4.3.3" 263 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" 264 | integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== 265 | dependencies: 266 | ms "2.1.2" 267 | 268 | decamelize@^4.0.0: 269 | version "4.0.0" 270 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" 271 | integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== 272 | 273 | deepmerge@^4.2.2: 274 | version "4.2.2" 275 | resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" 276 | integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== 277 | 278 | diff@5.0.0: 279 | version "5.0.0" 280 | resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" 281 | integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== 282 | 283 | emoji-regex@^8.0.0: 284 | version "8.0.0" 285 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 286 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 287 | 288 | escalade@^3.1.1: 289 | version "3.1.1" 290 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 291 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 292 | 293 | escape-string-regexp@4.0.0: 294 | version "4.0.0" 295 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 296 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 297 | 298 | escape-string-regexp@^1.0.5: 299 | version "1.0.5" 300 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 301 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 302 | 303 | estree-walker@^1.0.1: 304 | version "1.0.1" 305 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" 306 | integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== 307 | 308 | estree-walker@^2.0.1: 309 | version "2.0.2" 310 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" 311 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== 312 | 313 | fill-range@^7.0.1: 314 | version "7.0.1" 315 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 316 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 317 | dependencies: 318 | to-regex-range "^5.0.1" 319 | 320 | find-up@5.0.0: 321 | version "5.0.0" 322 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 323 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 324 | dependencies: 325 | locate-path "^6.0.0" 326 | path-exists "^4.0.0" 327 | 328 | flat@^5.0.2: 329 | version "5.0.2" 330 | resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" 331 | integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== 332 | 333 | fs.realpath@^1.0.0: 334 | version "1.0.0" 335 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 336 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 337 | 338 | fsevents@~2.3.2: 339 | version "2.3.2" 340 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 341 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 342 | 343 | function-bind@^1.1.1: 344 | version "1.1.1" 345 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 346 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 347 | 348 | get-caller-file@^2.0.5: 349 | version "2.0.5" 350 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 351 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 352 | 353 | glob-parent@~5.1.2: 354 | version "5.1.2" 355 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 356 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 357 | dependencies: 358 | is-glob "^4.0.1" 359 | 360 | glob@7.2.0, glob@^7.1.6: 361 | version "7.2.0" 362 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" 363 | integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== 364 | dependencies: 365 | fs.realpath "^1.0.0" 366 | inflight "^1.0.4" 367 | inherits "2" 368 | minimatch "^3.0.4" 369 | once "^1.3.0" 370 | path-is-absolute "^1.0.0" 371 | 372 | growl@1.10.5: 373 | version "1.10.5" 374 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" 375 | integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== 376 | 377 | has-flag@^3.0.0: 378 | version "3.0.0" 379 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 380 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 381 | 382 | has-flag@^4.0.0: 383 | version "4.0.0" 384 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 385 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 386 | 387 | has@^1.0.3: 388 | version "1.0.3" 389 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 390 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 391 | dependencies: 392 | function-bind "^1.1.1" 393 | 394 | he@1.2.0: 395 | version "1.2.0" 396 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 397 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 398 | 399 | inflight@^1.0.4: 400 | version "1.0.6" 401 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 402 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 403 | dependencies: 404 | once "^1.3.0" 405 | wrappy "1" 406 | 407 | inherits@2: 408 | version "2.0.4" 409 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 410 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 411 | 412 | is-binary-path@~2.1.0: 413 | version "2.1.0" 414 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 415 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 416 | dependencies: 417 | binary-extensions "^2.0.0" 418 | 419 | is-core-module@^2.8.1: 420 | version "2.8.1" 421 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" 422 | integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== 423 | dependencies: 424 | has "^1.0.3" 425 | 426 | is-extglob@^2.1.1: 427 | version "2.1.1" 428 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 429 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 430 | 431 | is-fullwidth-code-point@^3.0.0: 432 | version "3.0.0" 433 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 434 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 435 | 436 | is-glob@^4.0.1, is-glob@~4.0.1: 437 | version "4.0.3" 438 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 439 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 440 | dependencies: 441 | is-extglob "^2.1.1" 442 | 443 | is-module@^1.0.0: 444 | version "1.0.0" 445 | resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" 446 | integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= 447 | 448 | is-number@^7.0.0: 449 | version "7.0.0" 450 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 451 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 452 | 453 | is-plain-obj@^2.1.0: 454 | version "2.1.0" 455 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 456 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 457 | 458 | is-reference@^1.2.1: 459 | version "1.2.1" 460 | resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" 461 | integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== 462 | dependencies: 463 | "@types/estree" "*" 464 | 465 | is-unicode-supported@^0.1.0: 466 | version "0.1.0" 467 | resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" 468 | integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== 469 | 470 | isexe@^2.0.0: 471 | version "2.0.0" 472 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 473 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 474 | 475 | jest-worker@^26.2.1: 476 | version "26.6.2" 477 | resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" 478 | integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== 479 | dependencies: 480 | "@types/node" "*" 481 | merge-stream "^2.0.0" 482 | supports-color "^7.0.0" 483 | 484 | js-tokens@^4.0.0: 485 | version "4.0.0" 486 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 487 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 488 | 489 | js-yaml@4.1.0: 490 | version "4.1.0" 491 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 492 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 493 | dependencies: 494 | argparse "^2.0.1" 495 | 496 | kleur@^3.0.3: 497 | version "3.0.3" 498 | resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" 499 | integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== 500 | 501 | locate-path@^6.0.0: 502 | version "6.0.0" 503 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 504 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 505 | dependencies: 506 | p-locate "^5.0.0" 507 | 508 | lodash@^4.17.21: 509 | version "4.17.21" 510 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 511 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 512 | 513 | log-symbols@4.1.0: 514 | version "4.1.0" 515 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" 516 | integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== 517 | dependencies: 518 | chalk "^4.1.0" 519 | is-unicode-supported "^0.1.0" 520 | 521 | magic-string@^0.25.7: 522 | version "0.25.7" 523 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" 524 | integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== 525 | dependencies: 526 | sourcemap-codec "^1.4.4" 527 | 528 | merge-stream@^2.0.0: 529 | version "2.0.0" 530 | resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" 531 | integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== 532 | 533 | minimatch@3.0.4: 534 | version "3.0.4" 535 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 536 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 537 | dependencies: 538 | brace-expansion "^1.1.7" 539 | 540 | minimatch@^3.0.4: 541 | version "3.1.2" 542 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 543 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 544 | dependencies: 545 | brace-expansion "^1.1.7" 546 | 547 | mocha@^9.2.1: 548 | version "9.2.1" 549 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.1.tgz#a1abb675aa9a8490798503af57e8782a78f1338e" 550 | integrity sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ== 551 | dependencies: 552 | "@ungap/promise-all-settled" "1.1.2" 553 | ansi-colors "4.1.1" 554 | browser-stdout "1.3.1" 555 | chokidar "3.5.3" 556 | debug "4.3.3" 557 | diff "5.0.0" 558 | escape-string-regexp "4.0.0" 559 | find-up "5.0.0" 560 | glob "7.2.0" 561 | growl "1.10.5" 562 | he "1.2.0" 563 | js-yaml "4.1.0" 564 | log-symbols "4.1.0" 565 | minimatch "3.0.4" 566 | ms "2.1.3" 567 | nanoid "3.2.0" 568 | serialize-javascript "6.0.0" 569 | strip-json-comments "3.1.1" 570 | supports-color "8.1.1" 571 | which "2.0.2" 572 | workerpool "6.2.0" 573 | yargs "16.2.0" 574 | yargs-parser "20.2.4" 575 | yargs-unparser "2.0.0" 576 | 577 | ms@2.1.2: 578 | version "2.1.2" 579 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 580 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 581 | 582 | ms@2.1.3: 583 | version "2.1.3" 584 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 585 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 586 | 587 | nanoid@3.2.0: 588 | version "3.2.0" 589 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" 590 | integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== 591 | 592 | normalize-path@^3.0.0, normalize-path@~3.0.0: 593 | version "3.0.0" 594 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 595 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 596 | 597 | once@^1.3.0: 598 | version "1.4.0" 599 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 600 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 601 | dependencies: 602 | wrappy "1" 603 | 604 | p-limit@^3.0.2: 605 | version "3.1.0" 606 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 607 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 608 | dependencies: 609 | yocto-queue "^0.1.0" 610 | 611 | p-locate@^5.0.0: 612 | version "5.0.0" 613 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 614 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 615 | dependencies: 616 | p-limit "^3.0.2" 617 | 618 | path-exists@^4.0.0: 619 | version "4.0.0" 620 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 621 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 622 | 623 | path-is-absolute@^1.0.0: 624 | version "1.0.1" 625 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 626 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 627 | 628 | path-parse@^1.0.7: 629 | version "1.0.7" 630 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 631 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 632 | 633 | picocolors@^1.0.0: 634 | version "1.0.0" 635 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 636 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 637 | 638 | picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: 639 | version "2.3.1" 640 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 641 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 642 | 643 | prompts@^2.4.2: 644 | version "2.4.2" 645 | resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" 646 | integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== 647 | dependencies: 648 | kleur "^3.0.3" 649 | sisteransi "^1.0.5" 650 | 651 | randombytes@^2.1.0: 652 | version "2.1.0" 653 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 654 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 655 | dependencies: 656 | safe-buffer "^5.1.0" 657 | 658 | readdirp@~3.6.0: 659 | version "3.6.0" 660 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 661 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 662 | dependencies: 663 | picomatch "^2.2.1" 664 | 665 | require-directory@^2.1.1: 666 | version "2.1.1" 667 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 668 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 669 | 670 | resolve@^1.17.0, resolve@^1.19.0: 671 | version "1.22.0" 672 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" 673 | integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== 674 | dependencies: 675 | is-core-module "^2.8.1" 676 | path-parse "^1.0.7" 677 | supports-preserve-symlinks-flag "^1.0.0" 678 | 679 | rollup-plugin-terser@^7.0.2: 680 | version "7.0.2" 681 | resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" 682 | integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== 683 | dependencies: 684 | "@babel/code-frame" "^7.10.4" 685 | jest-worker "^26.2.1" 686 | serialize-javascript "^4.0.0" 687 | terser "^5.0.0" 688 | 689 | rollup@^2.67.3: 690 | version "2.67.3" 691 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.67.3.tgz#3f04391fc296f807d067c9081d173e0a33dbd37e" 692 | integrity sha512-G/x1vUwbGtP6O5ZM8/sWr8+p7YfZhI18pPqMRtMYMWSbHjKZ/ajHGiM+GWNTlWyOR0EHIdT8LHU+Z4ciIZ1oBw== 693 | optionalDependencies: 694 | fsevents "~2.3.2" 695 | 696 | safe-buffer@^5.1.0: 697 | version "5.2.1" 698 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 699 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 700 | 701 | serialize-javascript@6.0.0: 702 | version "6.0.0" 703 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" 704 | integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== 705 | dependencies: 706 | randombytes "^2.1.0" 707 | 708 | serialize-javascript@^4.0.0: 709 | version "4.0.0" 710 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" 711 | integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== 712 | dependencies: 713 | randombytes "^2.1.0" 714 | 715 | sisteransi@^1.0.5: 716 | version "1.0.5" 717 | resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" 718 | integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== 719 | 720 | source-map-support@~0.5.20: 721 | version "0.5.21" 722 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 723 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 724 | dependencies: 725 | buffer-from "^1.0.0" 726 | source-map "^0.6.0" 727 | 728 | source-map@^0.6.0: 729 | version "0.6.1" 730 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 731 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 732 | 733 | source-map@~0.7.2: 734 | version "0.7.3" 735 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" 736 | integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== 737 | 738 | sourcemap-codec@^1.4.4: 739 | version "1.4.8" 740 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" 741 | integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== 742 | 743 | string-width@^4.1.0, string-width@^4.2.0: 744 | version "4.2.3" 745 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 746 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 747 | dependencies: 748 | emoji-regex "^8.0.0" 749 | is-fullwidth-code-point "^3.0.0" 750 | strip-ansi "^6.0.1" 751 | 752 | strip-ansi@^6.0.0, strip-ansi@^6.0.1: 753 | version "6.0.1" 754 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 755 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 756 | dependencies: 757 | ansi-regex "^5.0.1" 758 | 759 | strip-json-comments@3.1.1: 760 | version "3.1.1" 761 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 762 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 763 | 764 | supports-color@8.1.1: 765 | version "8.1.1" 766 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" 767 | integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== 768 | dependencies: 769 | has-flag "^4.0.0" 770 | 771 | supports-color@^5.3.0: 772 | version "5.5.0" 773 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 774 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 775 | dependencies: 776 | has-flag "^3.0.0" 777 | 778 | supports-color@^7.0.0, supports-color@^7.1.0: 779 | version "7.2.0" 780 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 781 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 782 | dependencies: 783 | has-flag "^4.0.0" 784 | 785 | supports-preserve-symlinks-flag@^1.0.0: 786 | version "1.0.0" 787 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 788 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 789 | 790 | terser@^5.0.0: 791 | version "5.10.0" 792 | resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" 793 | integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== 794 | dependencies: 795 | commander "^2.20.0" 796 | source-map "~0.7.2" 797 | source-map-support "~0.5.20" 798 | 799 | to-regex-range@^5.0.1: 800 | version "5.0.1" 801 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 802 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 803 | dependencies: 804 | is-number "^7.0.0" 805 | 806 | which@2.0.2: 807 | version "2.0.2" 808 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 809 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 810 | dependencies: 811 | isexe "^2.0.0" 812 | 813 | workerpool@6.2.0: 814 | version "6.2.0" 815 | resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" 816 | integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== 817 | 818 | wrap-ansi@^7.0.0: 819 | version "7.0.0" 820 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 821 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 822 | dependencies: 823 | ansi-styles "^4.0.0" 824 | string-width "^4.1.0" 825 | strip-ansi "^6.0.0" 826 | 827 | wrappy@1: 828 | version "1.0.2" 829 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 830 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 831 | 832 | y18n@^5.0.5: 833 | version "5.0.8" 834 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" 835 | integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 836 | 837 | yargs-parser@20.2.4: 838 | version "20.2.4" 839 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" 840 | integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== 841 | 842 | yargs-parser@^20.2.2: 843 | version "20.2.9" 844 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" 845 | integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== 846 | 847 | yargs-unparser@2.0.0: 848 | version "2.0.0" 849 | resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" 850 | integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== 851 | dependencies: 852 | camelcase "^6.0.0" 853 | decamelize "^4.0.0" 854 | flat "^5.0.2" 855 | is-plain-obj "^2.1.0" 856 | 857 | yargs@16.2.0: 858 | version "16.2.0" 859 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" 860 | integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== 861 | dependencies: 862 | cliui "^7.0.2" 863 | escalade "^3.1.1" 864 | get-caller-file "^2.0.5" 865 | require-directory "^2.1.1" 866 | string-width "^4.2.0" 867 | y18n "^5.0.5" 868 | yargs-parser "^20.2.2" 869 | 870 | yocto-queue@^0.1.0: 871 | version "0.1.0" 872 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 873 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 874 | -------------------------------------------------------------------------------- /packages/processor/README.md: -------------------------------------------------------------------------------- 1 | # [@feature-sliced/eslint-plugin-messages](https://www.npmjs.com/package/@feature-sliced/eslint-plugin-messages) 2 | 3 | > `WIP:` At the moment at beta-testing - [use carefully](https://github.com/feature-sliced/eslint-config/discussions/75) 4 | 5 | 6 | 7 | Custom messages processing for [@feature-sliced/eslint-config](https://www.npmjs.com/package/@feature-sliced/eslint-config) 8 | 9 | - Methodology specific messages 10 | - Only important details 11 | - With documentation references 12 | 13 | ## Get Started 14 | 15 | 1. You'll first need to setup [@feature-sliced/eslint-config](https://www.npmjs.com/package/@feature-sliced/eslint-config) 16 | 17 | 2. Next, install `@feature-sliced/eslint-plugin-messages` 18 | 19 | ```sh 20 | $ npm install -D @feature-sliced/eslint-plugin-messages 21 | # or by yarn 22 | $ yarn add -D @feature-sliced/eslint-plugin-messages 23 | ``` 24 | 25 | 3. Add config to the `plugins` and `processor` sections of your `.eslintrc` configuration file: 26 | 27 | ```json 28 | { 29 | "plugins": [ 30 | ... 31 | "@feature-sliced/eslint-plugin-messages" 32 | ], 33 | "processor": "@feature-sliced/messages/fs", 34 | } 35 | ``` 36 | 37 | 4. See upgraded messages 🍰 38 | 39 | ```js 40 | // Before 41 | > '"widgets" is not allowed to import "widgets" | See rules: https://feature-sliced.design/docs/reference/layers/overview' 42 | > 'Reaching to "features/search/ui" is not allowed.' 43 | > 'entities/auth/model` import should occur before import of `shared/config' 44 | // After 45 | > 'Violated isolation between layers or slices: "widgets" => "widgets" | https://git.io/Jymh2' 46 | > 'Violated usage of modules Public API | https://git.io/Jymjf' 47 | > 'Broken order of imports | https://git.io/JymjI' 48 | ``` 49 | 50 | ## FAQ 51 | 52 | ### Why processor as plugin? 53 | 54 | Because of [ESlint restrictions](https://eslint.org/docs/developer-guide/working-with-plugins#processors-in-plugins) 55 | -------------------------------------------------------------------------------- /packages/processor/index.js: -------------------------------------------------------------------------------- 1 | const { patchMessage } = require("./messages"); 2 | 3 | module.exports = { 4 | processors: { 5 | "fs": { 6 | postprocess: function (messages, ...all) { 7 | return messages[0].map((message) => { 8 | return patchMessage(message); 9 | }); 10 | }, 11 | supportsAutofix: true, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/processor/messages.js: -------------------------------------------------------------------------------- 1 | // TODO: https://gist.github.com/Krakazybik/53cebb2c763305be13e31042d59a7c72#file-gistfile1-js-L31 2 | 3 | const getRuleMessage = (msg) => { 4 | switch (msg.ruleId) { 5 | case 'import/order': { 6 | return { 7 | message: 'Broken order of imports | https://git.io/JymjI', 8 | } 9 | } 10 | case 'import/no-internal-modules': { 11 | return { 12 | message: 'Violated usage of modules Public API | https://git.io/Jymjf', 13 | } 14 | } 15 | case 'boundaries/element-types': { 16 | const { groups } = msg.message.match(/(?"\S+").+(?"\S+")/i); 17 | const from = groups?.from || ''; 18 | const to = groups?.to || ''; 19 | return { 20 | message: `Violated isolation between layers or slices: ${from} => ${to} | https://git.io/Jymh2`, 21 | } 22 | } 23 | } 24 | } 25 | 26 | const patchMessage = (rawMsg) => { 27 | const msg = getRuleMessage(rawMsg); 28 | if (!msg) return rawMsg; 29 | 30 | return { ...rawMsg, ...msg}; 31 | } 32 | 33 | module.exports = { patchMessage }; 34 | -------------------------------------------------------------------------------- /packages/processor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@feature-sliced/eslint-plugin-messages", 3 | "version": "0.1.0-beta.2", 4 | "description": "🍰 Custom messages processor", 5 | "keywords": [ 6 | "eslint", 7 | "eslintconfig", 8 | "eslint-config", 9 | "feature-sliced", 10 | "feature-slices", 11 | "feature-driven", 12 | "feature-based" 13 | ], 14 | "main": "index.js", 15 | "repository": "https://github.com/feature-sliced/eslint-config.git", 16 | "author": "FeatureSliced core-team", 17 | "license": "MIT" 18 | } 19 | -------------------------------------------------------------------------------- /rules/import-order/README.md: -------------------------------------------------------------------------------- 1 | # @feature-sliced/import-order 2 | 3 | #### Reference: [Layers](https://feature-sliced.design/docs/reference/layers) 4 | 5 | ## Usage 6 | 7 | Add `"@feature-sliced/eslint-config/rules/import-order"` to your `extends` section in ESLint config. 8 | 9 | ```js 10 | // 👎 Fail 11 | import { getSmth } from "./lib"; 12 | import axios from "axios"; 13 | import { data } from "../fixtures"; 14 | import { authModel } from "entities/auth"; 15 | import { Button } from "shared/ui"; 16 | import { LoginForm } from "features/login-form"; 17 | import { Header } from "widgets/header"; 18 | import { debounce } from "shared/lib/fp"; 19 | 20 | // 👍 Pass 21 | import axios from "axios"; // 1) external libs 22 | import { Header } from "widgets/header"; // 2.1) Layers: widgets 23 | import { LoginForm } from "features/login-form"; // 2.2) Layers: features 24 | import { authModel } from "entities/auth"; // 2.3) Layers: entities 25 | import { Button } from "shared/ui"; // 2.4) Layers: shared 26 | import { debounce } from "shared/lib/fp"; // 2.4) Layers: shared 27 | import { data } from "../fixtures"; // 3) parent 28 | import { getSmth } from "./lib"; // 4) sibling 29 | ``` 30 | 31 | > `WARN:` Rule supports layer-based imports, but [its recommended](../public-api) to prefer more specified imports 32 | > 33 | > ```js 34 | > import { ... } from "shared"; // Non-critical 35 | > import { ... } from "shared/ui"; // Better 36 | > import { ... } from "shared/ui/button"; // Perfect 37 | > ``` 38 | 39 | ## Experimental 40 | 41 | **With reversed order ("from abstract to specific") and spaces between layers groups** 42 | [(why experimental?)](https://github.com/feature-sliced/eslint-config/issues/85) 43 | 44 | Add `"@feature-sliced/eslint-config/rules/import-order/experimental"` to your `extends` section in ESLint config. 45 | 46 | *Only for @^0.1.0-beta.4* 47 | 48 | ```js 49 | import axios from "axios"; // 1) external libs 50 | 51 | import { debounce } from "shared/lib/fp"; // 2.1) Layers: shared 52 | import { Button } from "shared/ui"; // 2.1) Layers: shared 53 | 54 | import { authModel } from "entities/auth"; // 2.2) Layers: entities 55 | 56 | import { LoginForm } from "features/login-form"; // 2.3) Layers: features 57 | 58 | import { Header } from "widgets/header"; // 2.4) Layers: widgets 59 | 60 | import { data } from "../fixtures"; // 3) parent 61 | import { getSmth } from "./lib"; // 4) sibling 62 | ``` 63 | -------------------------------------------------------------------------------- /rules/import-order/experimental.js: -------------------------------------------------------------------------------- 1 | const { layersLib } = require("../../utils"); 2 | const REVERSED_FS_LAYERS = [...layersLib.FS_LAYERS].reverse(); 3 | 4 | module.exports = { 5 | plugins: [ 6 | "import", 7 | ], 8 | rules: { 9 | "import/order": [ 10 | 2, 11 | { 12 | alphabetize: { 13 | order: 'asc', 14 | caseInsensitive: true, 15 | }, 16 | pathGroupsExcludedImportTypes: ["builtin"], 17 | groups: ["builtin", "external", "internal", "parent", "sibling", "index"], 18 | 19 | // experimental features 20 | 'newlines-between': 'always', 21 | pathGroups: REVERSED_FS_LAYERS.map( 22 | (layer) => ({ 23 | pattern: `**/?(*)${layer}{,/**}`, 24 | group: "internal", 25 | position: "after", 26 | }), 27 | ), 28 | }, 29 | ], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /rules/import-order/experimental.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("./experimental"); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser(cfg), 9 | }); 10 | 11 | describe("Import order experimental:", () => { 12 | 13 | it("should lint with errors.", async () => { 14 | const report = await eslint.lintText(` 15 | import { Cart } from "@/entities/cart"; 16 | import { Input } from "~/shared/ui"; 17 | import { getSmth } from "./lib"; 18 | import axios from "axios"; 19 | import { data } from "../fixtures"; 20 | import { authModel } from "entities/auth"; 21 | import { Button } from "shared/ui"; 22 | import { LoginForm } from "features/login-form"; 23 | import { Header } from "widgets/header"; 24 | import { debounce } from "shared/lib/fp"; 25 | import { One } from "@entities/one"; 26 | import { Two } from "~entities/two"; 27 | `); 28 | 29 | assert.strictEqual(report[0].errorCount, 18); 30 | }); 31 | 32 | it("should lint without errors.", async () => { 33 | const report = await eslint.lintText(` 34 | import axios from "axios"; 35 | 36 | import { Shared } from "shared"; 37 | import { debounce } from "shared/lib/fp"; 38 | import { model } from "shared/model"; 39 | import { Button } from "shared/ui"; 40 | 41 | import { globalEntities } from "entities"; 42 | import { authModel } from "entities/auth"; 43 | import { Cart } from "entities/cart"; 44 | import { One } from "entities/one"; 45 | import { Two } from "entities/two"; 46 | 47 | import { LoginForm } from "features/login-form"; 48 | 49 | import { Widgets } from "widgets"; 50 | import { Header } from "widgets/header"; 51 | import { Zero } from "widgets/zero"; 52 | 53 | import { data } from "../fixtures"; 54 | 55 | import { getSmth } from "./lib"; 56 | `); 57 | 58 | assert.strictEqual(report[0].errorCount, 0); 59 | }); 60 | 61 | 62 | it("~aliased should lint without errors.", async () => { 63 | const report = await eslint.lintText(` 64 | import axios from "axios"; 65 | 66 | import { debounce } from "~shared/lib/fp"; 67 | import { model } from "~shared/model"; 68 | import { Button } from "~shared/ui"; 69 | 70 | import { authModel } from "~entities/auth"; 71 | import { Cart } from "~entities/cart"; 72 | import { One } from "~entities/one"; 73 | import { Two } from "~entities/two"; 74 | 75 | import { LoginForm } from "~features/login-form"; 76 | 77 | import { Widgets } from "~widgets"; 78 | import { Header } from "~widgets/header"; 79 | import { Zero } from "~widgets/zero"; 80 | 81 | import { data } from "../fixtures"; 82 | 83 | import { getSmth } from "./lib"; 84 | `); 85 | 86 | assert.strictEqual(report[0].errorCount, 0); 87 | }); 88 | 89 | 90 | it("~/aliased should lint without errors.", async () => { 91 | const report = await eslint.lintText(` 92 | import axios from "axios"; 93 | 94 | import { debounce } from "~/shared/lib/fp"; 95 | import { model } from "~/shared/model"; 96 | import { Button } from "~/shared/ui"; 97 | 98 | import { authModel } from "~/entities/auth"; 99 | import { Cart } from "~/entities/cart"; 100 | import { One } from "~/entities/one"; 101 | import { Two } from "~/entities/two"; 102 | 103 | import { LoginForm } from "~/features/login-form"; 104 | 105 | import { Widgets } from "~/widgets"; 106 | import { Header } from "~/widgets/header"; 107 | import { Zero } from "~/widgets/zero"; 108 | 109 | import { data } from "../fixtures"; 110 | 111 | import { getSmth } from "./lib"; 112 | `); 113 | 114 | assert.strictEqual(report[0].errorCount, 0); 115 | }); 116 | 117 | 118 | it("should be alphabetic", async () => { 119 | const report = await eslint.lintText(` 120 | import { Apple } from 'features/apple'; 121 | import { Bee } from 'features/bee'; 122 | import { Cord } from 'features/cord'; 123 | import { Dream } from 'features/dream'; 124 | `); 125 | 126 | assert.strictEqual(report[0].errorCount, 0); 127 | }); 128 | 129 | it("should be alphabetic error", async () => { 130 | const report = await eslint.lintText(` 131 | import { Dream } from 'features/dream'; 132 | import { Cord } from 'features/cord'; 133 | import { Bee } from 'features/bee'; 134 | import { Apple } from 'features/apple'; 135 | `); 136 | 137 | assert.strictEqual(report[0].errorCount, 3); 138 | }); 139 | 140 | it("should be with spaces between layers", async () => { 141 | const report = await eslint.lintText(` 142 | import { Dream } from 'shared/dream'; 143 | 144 | import { Cord } from 'entities/cord'; 145 | 146 | import { Bee } from 'features/bee'; 147 | 148 | import { Apple } from 'app/apple'; 149 | `); 150 | 151 | assert.strictEqual(report[0].errorCount, 0); 152 | }); 153 | 154 | it("should be with spaces between layers errors", async () => { 155 | const report = await eslint.lintText(` 156 | import React from 'react'; 157 | import { Dream } from 'shared/dream'; 158 | import { Cord } from 'entities/cord'; 159 | import { Bee } from 'features/bee'; 160 | import { Apple } from 'app/apple'; 161 | `); 162 | 163 | assert.strictEqual(report[0].errorCount, 4); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /rules/import-order/index.js: -------------------------------------------------------------------------------- 1 | const { layersLib } = require("../../utils"); 2 | 3 | module.exports = { 4 | plugins: [ 5 | "import", 6 | ], 7 | rules: { 8 | "import/order": [ 9 | 2, 10 | { 11 | alphabetize: { 12 | order: 'asc', 13 | caseInsensitive: true, 14 | }, 15 | pathGroups: layersLib.FS_LAYERS.map( 16 | (layer) => ({ 17 | pattern: `**/?(*)${layer}{,/**}`, 18 | group: "internal", 19 | position: "after", 20 | }), 21 | ), 22 | pathGroupsExcludedImportTypes: ["builtin"], 23 | groups: ["builtin", "external", "internal", "parent", "sibling", "index"], 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /rules/import-order/index.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("./"); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser(cfg), 9 | }); 10 | 11 | describe("Import order:", () => { 12 | 13 | it("should lint with errors.", async () => { 14 | const report = await eslint.lintText(` 15 | import { Cart } from "@/entities/cart"; // 5 16 | import { Input } from "~/shared/ui"; // 3.1 17 | import { getSmth } from "./lib"; // 1 18 | import axios from "axios"; // 10 19 | import { data } from "../fixtures"; // 2 20 | import { authModel } from "entities/auth"; // 5 21 | import { Button } from "shared/ui"; // 4 22 | import { LoginForm } from "features/login-form"; // 8 23 | import { Header } from "widgets/header"; // 9 24 | import { debounce } from "shared/lib/fp"; // 3 25 | import { One } from "@entities/one"; // 6 26 | import { Two } from "~entities/two"; // 7 27 | `); 28 | 29 | assert.strictEqual(report[0].errorCount, 8); 30 | }); 31 | 32 | it("should lint without errors.", async () => { 33 | const report = await eslint.lintText(` 34 | // warn: specific order in mixed alias ~/layer => ~layer => layer 35 | import axios from "axios"; // 1) external libs 36 | import { Header } from "widgets/header"; // 2.1) Layers: widgets 37 | import { Zero } from "widgets/zero"; // 2.1) Layers: widget 38 | import { LoginForm } from "features/login-form"; // 2.2) Layers: features 39 | import { globalEntities } from "entities"; // 2.4) Layers: entities 40 | import { authModel } from "entities/auth"; // 2.4) Layers: entities 41 | import { Cart } from "entities/cart"; // 2.4) Layers: entities 42 | import { One } from "entities/one"; // 2.4) Layers: entities 43 | import { Two } from "entities/two"; // 2.4) Layers: entities 44 | import { debounce } from "shared/lib/fp"; // 2.5) Layers: shared 45 | import { Button } from "shared/ui"; // 2.5) Layers: shared 46 | import { Input } from "shared/ui"; // 2.5) Layers: shared 47 | import { data } from "../fixtures"; // 3) parent 48 | import { getSmth } from "./lib"; // 4) sibling 49 | `); 50 | 51 | assert.strictEqual(report[0].errorCount, 0); 52 | }); 53 | 54 | 55 | it("should lint without errors.", async () => { 56 | const report = await eslint.lintText(` 57 | // warn: specific order in mixed alias ~/layer => ~layer => layer 58 | // not used in real, but test aliases support 59 | import axios from "axios"; // 1) external libs 60 | import { Zero } from "@widgets/zero"; // 2.1) Layers: widget - Alias 61 | import { Widgets } from "widgets"; // 2.1) Layers: widgets 62 | import { Header } from "widgets/header"; // 2.1) Layers: widgets 63 | import { LoginForm } from "features/login-form"; // 2.2) Layers: features 64 | import { Cart } from "@/entities/cart"; // 2.3) Layers: entities - Alias 65 | import { One } from "@entities/one"; // 2.3) Layers: entities - Alias 66 | import { Two } from "@entities/two"; // 2.3) Layers: entities - Alias 67 | import { authModel } from "entities/auth"; // 2.3) Layers: entities 68 | import { Shared } from "shared"; // 2.4) Layers: shared 69 | import { debounce } from "shared/lib/fp"; // 2.4) Layers: shared 70 | import { Button } from "shared/ui"; // 2.4) Layers: shared 71 | import { Input } from "~/shared/ui"; // 2.4) Layers: shared - Alias 72 | import { data } from "../fixtures"; // 3) parent 73 | import { getSmth } from "./lib"; // 4) sibling 74 | `); 75 | 76 | assert.strictEqual(report[0].errorCount, 0); 77 | }); 78 | 79 | it("aliased layers should lint with errors.", async () => { 80 | const report = await eslint.lintText(` 81 | import { Third } from '@shared/third'; 82 | import { Second } from '@entities/second'; 83 | import { First } from '@features/first'; 84 | `); 85 | 86 | assert.strictEqual(report[0].errorCount, 2); 87 | }); 88 | 89 | it("aliased layers should lint without errors.", async () => { 90 | const report = await eslint.lintText(` 91 | import { Widgets } from "@widgets"; 92 | import { First } from '@features/first'; 93 | import { Second } from '@entities/second'; 94 | import { Third } from '@shared/third'; 95 | `); 96 | 97 | assert.strictEqual(report[0].errorCount, 0); 98 | }); 99 | 100 | it("~aliased should lint without errors.", async () => { 101 | const report = await eslint.lintText(` 102 | import axios from "axios"; 103 | import { Widgets } from "~widgets"; 104 | import { Header } from "~widgets/header"; 105 | import { Zero } from "~widgets/zero"; 106 | import { LoginForm } from "~features/login-form"; 107 | import { authModel } from "~entities/auth"; 108 | import { Cart } from "~entities/cart"; 109 | import { One } from "~entities/one"; 110 | import { Two } from "~entities/two"; 111 | import { debounce } from "~shared/lib/fp"; 112 | import { model } from "~shared/model"; 113 | import { Button } from "~shared/ui"; 114 | import { data } from "../fixtures"; 115 | import { getSmth } from "./lib"; 116 | `); 117 | 118 | assert.strictEqual(report[0].errorCount, 0); 119 | }); 120 | 121 | 122 | it("~/aliased should lint without errors.", async () => { 123 | const report = await eslint.lintText(` 124 | import axios from "axios"; 125 | import { Widgets } from "~/widgets"; 126 | import { Header } from "~/widgets/header"; 127 | import { Zero } from "~/widgets/zero"; 128 | import { LoginForm } from "~/features/login-form"; 129 | import { authModel } from "~/entities/auth"; 130 | import { Cart } from "~/entities/cart"; 131 | import { One } from "~/entities/one"; 132 | import { Two } from "~/entities/two"; 133 | import { debounce } from "~/shared/lib/fp"; 134 | import { model } from "~/shared/model"; 135 | import { Button } from "~/shared/ui"; 136 | import { data } from "../fixtures"; 137 | import { getSmth } from "./lib"; 138 | `); 139 | 140 | assert.strictEqual(report[0].errorCount, 0); 141 | }); 142 | 143 | describe("Alphabetic sort feature:", () => { 144 | 145 | it("should be alphabetic", async () => { 146 | const report = await eslint.lintText(` 147 | import { Apple } from 'features/apple'; 148 | import { Bee } from 'features/bee'; 149 | import { Cord } from 'features/cord'; 150 | import { Dream } from 'features/dream'; 151 | `); 152 | 153 | assert.strictEqual(report[0].errorCount, 0); 154 | }); 155 | 156 | it("should be alphabetic error", async () => { 157 | const report = await eslint.lintText(` 158 | import { Dream } from 'features/dream'; 159 | import { Cord } from 'features/cord'; 160 | import { Bee } from 'features/bee'; 161 | import { Apple } from 'features/apple'; 162 | `); 163 | 164 | assert.strictEqual(report[0].errorCount, 3); 165 | }); 166 | 167 | }) 168 | 169 | }); 170 | -------------------------------------------------------------------------------- /rules/integration.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../utils"); 4 | const cfg = require("../"); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.mockImports(cfg), 9 | }); 10 | 11 | describe("Integration tests:", () => { 12 | it("Global config should lint with errors", async () => { 13 | const report = await eslint.lintText(` 14 | import { getSmth } from "./lib"; // import-order 15 | import axios from "axios"; 16 | import { data } from "../fixtures"; // import-order 17 | import { authModel } from "entities/auth"; // import-order 18 | import { Button } from "shared/ui"; // import-order 19 | import { LoginForm } from "features/login-form"; // import-order 20 | import { Header } from "widgets/header"; // import-order, import-boundaries 21 | import { debounce } from "shared/lib/fp"; // import-order 22 | import { AuthPage } from "pages/auth"; // import-boundaries 23 | import { IssueDetails } from "widgets/issue-details/ui/details"; // import-order, publicAPI 24 | `, { 25 | filePath: "src/widgets/mock/index.js", 26 | }); 27 | 28 | assert.strictEqual(report[0].errorCount, 11); 29 | }); 30 | 31 | it("Global config should lint without errors", async () => { 32 | const report = await eslint.lintText(` 33 | import { getRoute } from "pages/auth"; 34 | import { Header } from "widgets/header"; 35 | import { LoginForm } from "features/login-form"; 36 | import { Phone } from "features/login-form/phone"; 37 | import { Article } from "entities/article"; 38 | import { LoginAPI } from "shared/api"; 39 | import { Button } from "shared/ui/button"; 40 | import { model } from "../model"; 41 | import { styles } from "./styles.module.scss"; 42 | `, { filePath: "src/app/ui/index.js" }); 43 | 44 | assert.strictEqual(report[0].errorCount, 0); 45 | }); 46 | 47 | it("Global config should lint only with import-order error", async () => { 48 | const report = await eslint.lintText(` 49 | import { LoginAPI } from "shared/api"; 50 | import { getRoute } from "pages/auth"; 51 | `, { filePath: "src/app/ui/index.js" }); 52 | 53 | assert.strictEqual(report[0].errorCount, 1); 54 | }); 55 | 56 | it("Global config should lint only with layer error", async () => { 57 | const report = await eslint.lintText(` 58 | import { LoginForm } from "features/login-form"; 59 | `, { filePath: "src/entities/ui/index.js" }); 60 | 61 | assert.strictEqual(report[0].errorCount, 1); 62 | }); 63 | 64 | it("Global config should lint only with slice error", async () => { 65 | const report = await eslint.lintText(` 66 | import { Article } from "entities/article"; 67 | `, { filePath: "src/entities/avatar/ui/index.js" }); 68 | 69 | assert.strictEqual(report[0].errorCount, 1); 70 | }); 71 | 72 | it("Global config should lint only with PublicAPI error", async () => { 73 | const report = await eslint.lintText(` 74 | import { orderModel } from "entities/order/model"; 75 | `, { filePath: "src/features/profile/ui/index.js" }); 76 | 77 | assert.strictEqual(report[0].errorCount, 1); 78 | }); 79 | 80 | it("Global config should pass with global node_modules", async () => { 81 | const report = await eslint.lintText(` 82 | import { orderModel } from "home/work/npm/node_modules/packages/custom/ci/index.js"; 83 | import { Something } from "home/work/npm/node_modules/packages/fancy-ui-kiy/some/index.js"; 84 | import { useDelay } from "home/work/npm/node_modules/packages/reduxium/use-delay/index.js"; 85 | `, { filePath: "src/features/profile/ui/index.js" }); 86 | 87 | assert.strictEqual(report[0].errorCount, 0); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /rules/integration.ts.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../utils"); 4 | const cfg = require("../"); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setTSParser( 9 | configLib.mockImports(cfg, "ts") 10 | ), 11 | }); 12 | 13 | describe("TypeScript integration tests:", () => { 14 | it("Global config with TS should lint with errors", async () => { 15 | const report = await eslint.lintText(` 16 | import { getSmth } from "./lib"; // import-order 17 | import axios from "axios"; 18 | import { data } from "../fixtures"; // import-order 19 | import { authModel } from "entities/auth"; // import-order 20 | import { Button } from "shared/ui"; // import-order 21 | import { LoginForm } from "features/login-form"; // import-order 22 | import { Header } from "widgets/header"; // import-order, import-boundaries 23 | import { debounce } from "shared/lib/fp"; // import-order 24 | import { AuthPage } from "pages/auth"; // import-boundaries 25 | import { IssueDetails } from "widgets/issue-details/ui/details"; // import-order, publicAPI 26 | 27 | interface IConfig { 28 | path: string; 29 | }; 30 | 31 | const configs: Array = []; 32 | `, { 33 | filePath: "src/widgets/mock/index.ts", 34 | }); 35 | 36 | assert.strictEqual(report[0].errorCount, 11); 37 | }); 38 | 39 | it("Global config with TS should lint without errors", async () => { 40 | const report = await eslint.lintText(` 41 | import { getRoute } from "pages/auth"; 42 | import { Header } from "widgets/header"; 43 | import { LoginForm } from "features/login-form"; 44 | import { Phone } from "features/login-form/phone"; 45 | import { Article } from "entities/article"; 46 | import { LoginAPI } from "shared/api"; 47 | import { Button } from "shared/ui/button"; 48 | import { model } from "../model"; 49 | import { styles } from "./styles.module.scss"; 50 | 51 | interface IConfig { 52 | path: string; 53 | }; 54 | 55 | const configs: Array = []; 56 | `, { filePath: "src/app/ui/index.ts" }); 57 | 58 | assert.strictEqual(report[0].errorCount, 0); 59 | }); 60 | 61 | it("Global config with TS should lint only with import-order error", async () => { 62 | const report = await eslint.lintText(` 63 | import { LoginAPI } from "shared/api"; 64 | import { getRoute } from "pages/auth"; 65 | const configs: Array = []; 66 | `, { filePath: "src/app/ui/index.ts" }); 67 | 68 | assert.strictEqual(report[0].errorCount, 1); 69 | }); 70 | 71 | it("Global config with TS should lint only with layer error", async () => { 72 | const report = await eslint.lintText(` 73 | import { LoginForm } from "features/login-form"; 74 | const configs: Array = []; 75 | `, { filePath: "src/entities/ui/index.ts" }); 76 | 77 | assert.strictEqual(report[0].errorCount, 1); 78 | }); 79 | 80 | it("Global config with TS should lint only with slice error", async () => { 81 | const report = await eslint.lintText(` 82 | import { Article } from "entities/article"; 83 | const configs: Array = []; 84 | `, { filePath: "src/entities/avatar/ui/index.ts" }); 85 | 86 | assert.strictEqual(report[0].errorCount, 1); 87 | }); 88 | 89 | it("Global config with TS should lint only with PublicAPI error", async () => { 90 | const report = await eslint.lintText(` 91 | import { orderModel } from "entities/order/model"; 92 | const configs: Array = []; 93 | `, { filePath: "src/features/profile/ui/index.ts" }); 94 | 95 | assert.strictEqual(report[0].errorCount, 1); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /rules/layers-slices/README.md: -------------------------------------------------------------------------------- 1 | # @feature-sliced/layers-slices 2 | 3 | #### Reference: [Cross-communication](https://feature-sliced.design/docs/concepts/cross-communication) 4 | 5 | ## Usage 6 | 7 | Add `"@feature-sliced/eslint-config/rules/layers-slices"` to your `extends` section in ESLint config. 8 | 9 | ```js 10 | // 👎 Fail 11 | // 🛣 features/auth-form/index.ts 12 | import { getRoute } from "pages/auth"; 13 | import { getStore } from "app/store"; 14 | import { getAuthCtx } from "features/logout"; 15 | import { UserAvatar } from "features/viewer-picker"; 16 | 17 | // 👍 Pass 18 | // 🛣 features/auth-form/index.ts 19 | import { sessionModel } from "entities/session"; 20 | import { Form, Button } from "shared/ui"; 21 | import { getAuthCtx } from "entities/session"; 22 | import { UserAvatar } from "entities/user"; 23 | ``` 24 | 25 | --- 26 | 27 | > ⚠️ **DANGEROUS-mode**: Support service directories for slices by `_` prefix ([why?](https://github.com/feature-sliced/eslint-config/discussions/75#discussioncomment-2056223)) 28 | > 29 | > Use carefully and at your own risk 30 | > 31 | > ```js 32 | > import { ... } from "../HomePage"; 33 | > import { ... } from "../ProfilePage"; 34 | > 35 | > // Imported into ... 36 | > @path "app/**" // 🟩 valid (upper layer) 37 | > @path "shared/router" // 🟥 not valid (lower layer) 38 | > @path "pages/CartPage" // 🟥 not valid (sibling slice) 39 | > @path "pages/router" // 🟥 not valid (sibling slice) 40 | > @path "pages/_router" // 🟩 again valid (as service directory/slice) 41 | > ``` 42 | > 43 | > But still actual: 44 | > 45 | > ```js 46 | > @path "pages/_router" 47 | > import { ... } from "app" // 🟥 not valid (lower layer) 48 | > 49 | > @path "shared/lib" 50 | > import { ... } from "pages/_router" // 🟥 not valid (lower layer) 51 | > ``` 52 | > 53 | > *Only for @^0.1.0-beta.6* 54 | -------------------------------------------------------------------------------- /rules/layers-slices/index.js: -------------------------------------------------------------------------------- 1 | const { layersLib } = require("../../utils"); 2 | 3 | const getNotSharedLayersRules = () => 4 | layersLib.getUpperLayers("shared").map((layer) => ({ 5 | from: layer, 6 | allow: layersLib.getLowerLayers(layer), 7 | })); 8 | 9 | const slicelessLayerRules = [ 10 | { 11 | from: "shared", 12 | allow: "shared", 13 | }, 14 | { 15 | from: "app", 16 | allow: "app", 17 | } 18 | ]; 19 | 20 | const getLayersBoundariesElements = () => 21 | layersLib.FS_LAYERS.map((layer) => ({ 22 | type: layer, 23 | pattern: `${layer}/!(_*){,/*}`, 24 | mode: "folder", 25 | capture: ["slices"], 26 | })); 27 | 28 | const getGodModeRules = () => 29 | layersLib.FS_LAYERS.map((layer) => ({ 30 | from: `gm_${layer}`, 31 | allow: [layer, ...layersLib.getLowerLayers(layer)] 32 | })); 33 | 34 | const getGodModeElements = () => 35 | layersLib.FS_LAYERS.map((layer) => ({ 36 | type: `gm_${layer}`, 37 | pattern: `${layer}/_*`, 38 | mode: "folder", 39 | capture: ["slices"], 40 | })); 41 | 42 | module.exports = { 43 | plugins: ["boundaries"], 44 | extends: ["plugin:boundaries/recommended"], 45 | ignorePatterns: [".eslintrc.js"], 46 | settings: { 47 | "boundaries/elements": [...getLayersBoundariesElements(), ...getGodModeElements()], 48 | }, 49 | rules: { 50 | "boundaries/element-types": [ 51 | 2, 52 | { 53 | "default": "disallow", 54 | "message": "\"${file.type}\" is not allowed to import \"${dependency.type}\" | See rules: https://feature-sliced.design/docs/reference/layers/overview ", 55 | "rules": [...getNotSharedLayersRules(), ...slicelessLayerRules, ...getGodModeRules()], 56 | }, 57 | ], 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /rules/layers-slices/layers.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("./"); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser( 9 | configLib.mockImports(cfg) 10 | ), 11 | }); 12 | 13 | describe("Import boundaries between layers", () => { 14 | 15 | describe("IDDQD boundaries", () => { 16 | 17 | it("should lint without errors in GodMode for _computed entities", async () => { 18 | const report = await eslint.lintText(` 19 | import { userModel } from "entities/user"; 20 | import { getUser } from "shared/api/user-api"; 21 | `, 22 | {filePath: "src/entities/_computed/UserPost/model.js"}); 23 | 24 | assert.strictEqual(report[0].errorCount, 0); 25 | }); 26 | 27 | it("should lint with errors for computed entities", async () => { 28 | const report = await eslint.lintText(` 29 | import { userModel } from "entities/user"; 30 | `, 31 | {filePath: "src/entities/computed/UserPost/model.js"}); 32 | 33 | assert.strictEqual(report[0].errorCount, 1); 34 | }); 35 | 36 | it("should lint without errors in GodMode for pages", async () => { 37 | const report = await eslint.lintText(` 38 | import { FooPage } from "pages/foo"; 39 | import { BagFeature } from "features/bag"; 40 | import { format } from "shared/lib/format"; 41 | import { BarPage } from "../bar"; 42 | `, 43 | {filePath: "src/pages/_router/private.routes.js"}); 44 | 45 | assert.strictEqual(report[0].errorCount, 0); 46 | }); 47 | 48 | it("should lint with errors in GodMode for upper layers", async () => { 49 | const report = await eslint.lintText(` 50 | import { MainPage } from "pages/main"; 51 | import { UserFeature } from "features/user"; 52 | `, 53 | {filePath: "src/entities/_computed/UserPost/model.js"}); 54 | 55 | assert.strictEqual(report[0].errorCount, 2); 56 | }); 57 | 58 | 59 | it("should lint with errors without GodMode for pages", async () => { 60 | const report = await eslint.lintText(` 61 | import { FooPage } from "pages/foo"; 62 | `, 63 | {filePath: "src/pages/router/private.routes.js"}); 64 | 65 | assert.strictEqual(report[0].errorCount, 1); 66 | }); 67 | }) 68 | 69 | it("should lint with cross-import errors.", async () => { 70 | const wrongImports = [ 71 | `import { getRoute } from "pages/auth";`, 72 | `import { getStore } from "app/store";`, 73 | ]; 74 | 75 | const report = await eslint.lintText(wrongImports.join("\n"), { 76 | filePath: "src/shared/lib/index.js", 77 | }); 78 | assert.strictEqual(report[0].errorCount, wrongImports.length); 79 | }); 80 | 81 | it("should lint without errors.", async () => { 82 | const validCodeSnippet = [ 83 | `import { sessionModel } from "entities/session";`, 84 | `import { Form, Button } from "shared/ui";`, 85 | ].join("\n"); 86 | 87 | const report = await eslint.lintText(validCodeSnippet, { 88 | filePath: "src/app/ui/app.js", 89 | }); 90 | assert.strictEqual(report[0].errorCount, 0); 91 | }); 92 | 93 | it("should lint without errors when import from shared.", async () => { 94 | const validCodeSnippet = [ 95 | `import { API_TOKEN } from "shared/config"`, 96 | `import { Form } from "shared/ui";`, 97 | ].join("\n"); 98 | 99 | const report = await eslint.lintText(validCodeSnippet, { 100 | filePath: "src/shared/ui/button.js", 101 | }); 102 | assert.strictEqual(report[0].errorCount, 0); 103 | }); 104 | 105 | it("should lint without errors when import from app.", async () => { 106 | const validCodeSnippet = [ 107 | `import "app/styles/styles.css"`, 108 | `import { withProviders } from "app/providers"`, 109 | ].join("\n"); 110 | 111 | const report = await eslint.lintText(validCodeSnippet, { 112 | filePath: "src/app/ui/app.tsx", 113 | }); 114 | assert.strictEqual(report[0].errorCount, 0); 115 | }); 116 | 117 | it("should lint with errors when import from app.", async () => { 118 | const wrongImports = [ 119 | `import { withProviders } from "app/providers"`, 120 | ]; 121 | 122 | const report = await eslint.lintText(wrongImports.join("\n"), { 123 | filePath: "src/features/add-user/model.tsx", 124 | }); 125 | assert.strictEqual(report[0].errorCount, wrongImports.length); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /rules/layers-slices/slices.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("."); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser( 9 | configLib.mockImports(cfg) 10 | ), 11 | }); 12 | 13 | describe("Import boundaries between slices and layers", () => { 14 | 15 | it("should lint with cross-import errors between pages.", async () => { 16 | const wrongImports = [ 17 | `import { getAuthCtx } from "pages/logout";`, 18 | `import { UserAvatar } from "pages/auth";`, 19 | ]; 20 | 21 | const report = await eslint.lintText(wrongImports.join("\n"), { 22 | filePath: "src/pages/map/index.js", 23 | }); 24 | 25 | assert.strictEqual(report[0].errorCount, wrongImports.length); 26 | }); 27 | 28 | it("should lint with cross-import errors between widgets.", async () => { 29 | const wrongImports = [ 30 | `import { HeaderTitle } from "widgets/header";`, 31 | `import { Links } from "widgets/footer";`, 32 | ]; 33 | 34 | const report = await eslint.lintText(wrongImports.join("\n"), { 35 | filePath: "src/widgets/mock/index.js", 36 | }); 37 | 38 | assert.strictEqual(report[0].errorCount, wrongImports.length); 39 | }); 40 | 41 | it("should lint with cross-import errors between features.", async () => { 42 | const wrongImports = [ 43 | `import { getAuthCtx } from "features/logout";`, 44 | `import { UserAvatar } from "features/viewer-picker";`, 45 | ]; 46 | 47 | const report = await eslint.lintText(wrongImports.join("\n"), { 48 | filePath: "features/auth-form/index.js", 49 | }); 50 | 51 | assert.strictEqual(report[0].errorCount, wrongImports.length); 52 | }); 53 | 54 | it("should lint with cross-import errors between entities.", async () => { 55 | const wrongImports = [ 56 | `import { LoginForm } from "features/login-form";`, 57 | `import { Avatar } from "features/avatar";`, 58 | ]; 59 | 60 | const report = await eslint.lintText(wrongImports.join("\n"), { 61 | filePath: "src/entities/mock/index.js", 62 | }); 63 | 64 | assert.strictEqual(report[0].errorCount, wrongImports.length); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /rules/public-api/README.md: -------------------------------------------------------------------------------- 1 | # @feature-sliced/public-api 2 | 3 | #### Reference: [PublicAPI](https://feature-sliced.design/docs/concepts/public-api) 4 | 5 | ## Usage 6 | 7 | Add `"@feature-sliced/eslint-config/rules/public-api"` to your `extends` section in ESLint config. 8 | 9 | #### Slices PublicAPI 10 | 11 | ```js 12 | // 👎 Fail 13 | import { Issues } from "pages/issues/ui"; 14 | import { IssueDetails } from "widgets/issue-details/ui/details" 15 | import { AuthForm } from "features/auth-form/ui/form" 16 | import { Button } from "shared/ui/button/button"; 17 | import { saveOrder } from "entities/order/model/actions"; 18 | import { orderModel } from "entities/order/model"; 19 | import { TicketCard } from "@/entities/ticket/ui"; 20 | 21 | // 👍 Pass 22 | import { Issues } from "pages/issues"; 23 | import { IssueDetails } from "widgets/issue-details" 24 | import { AuthForm } from "features/auth-form" 25 | import { Button } from "shared/ui/button"; 26 | import { orderModel } from "entities/order"; 27 | import { TicketCard } from "@/entities/ticket"; 28 | import { AuthForm } from "features/auth/form" 29 | import { Button } from "shared/ui"; 30 | ``` 31 | 32 | #### Segments PublicAPI 33 | 34 | ```js 35 | // 👍 Pass 36 | /** @path features/smth/index.ts */ 37 | export { SubmitButton, SmthForm } from "./ui"; 38 | export * from "./model"; 39 | export * as smthModel from "./model"; 40 | export { selectSmthById, ... } from "./model"; 41 | 42 | // 👎 Fail 43 | /** @path features/smth/index.ts */ 44 | export { SubmitButton } from "./ui/button"; 45 | export { SmthForm } from "./ui/form"; 46 | export * from "./model/actions"; 47 | export { selectSmthById } from "./model/selectors"; 48 | ``` 49 | 50 | ## Lite 51 | 52 | **Without SegmentsAPI / InnerAPI restrictions** [(why experimental?)](https://github.com/feature-sliced/eslint-config/issues/90) 53 | 54 | Add `"@feature-sliced/eslint-config/rules/public-api/lite"` to your `extends` section in ESLint config. 55 | 56 | *Only for @^0.1.0-beta.5* 57 | 58 | #### Slices PublicAPI 59 | 60 | Without changes 61 | 62 | ```js 63 | // 👍 Pass 64 | import { orderModel } from "entities/order"; 65 | // 👎 Fail 66 | import { orderModel } from "entities/order/model"; 67 | ``` 68 | 69 | #### Segments PublicAPI 70 | 71 | Less restricted with segments 72 | 73 | ```js 74 | // 👍 Pass 75 | /** @path features/smth/index.ts */ 76 | export { SubmitButton, SmthForm } from "./ui"; 77 | export * from "./model"; 78 | export * as smthModel from "./model"; 79 | export { selectSmthById, ... } from "./model"; 80 | 81 | // 👍 Also Pass 82 | /** @path features/smth/index.ts */ 83 | export { SubmitButton } from "./ui/button"; 84 | export { SmthForm } from "./ui/form"; 85 | export * from "./model/actions"; 86 | export { selectSmthById } from "./model/selectors"; 87 | ``` 88 | 89 | --- 90 | 91 | > ⚠️ **DANGEROUS-mode**: Support custom segments at shared by `_` prefix ([why?](https://github.com/feature-sliced/eslint-config/discussions/75#discussioncomment-1972319)) 92 | > 93 | > Use carefully and at your own risk 94 | > 95 | > ```js 96 | > import { ... } from "shared/lib" // 🟩 valid 97 | > import { ... } from "shared/library" // 🟥 not valid 98 | > 99 | > import { ... } from "shared/_library" // 🟩 again valid 100 | > import { ... } from "shared/_library/fp" // 🟩 still valid 101 | > import { ... } from "shared/_library/fp/compose" // 🟥 don't be brash :) 102 | > ``` 103 | > 104 | > *Only for @^0.1.0-beta.6* 105 | -------------------------------------------------------------------------------- /rules/public-api/index.js: -------------------------------------------------------------------------------- 1 | const { layersLib } = require("../../utils"); 2 | 3 | const FS_SLICED_LAYERS_REG = layersLib.getUpperLayers("shared").join("|"); 4 | const FS_SEGMENTS_REG = [ 5 | ...layersLib.FS_SEGMENTS, 6 | ...layersLib.FS_SEGMENTS.map((seg) => `${seg}.*`), 7 | ].join('|'); 8 | 9 | module.exports = { 10 | plugins: ["import"], 11 | rules: { 12 | "import/no-internal-modules": [ 13 | "error", { 14 | "allow": [ 15 | /** 16 | * Allow not segments import from slices 17 | * @example 18 | * 'entities/form/ui' // Fail 19 | * 'entities/form' // Pass 20 | */ 21 | `**/*(${FS_SLICED_LAYERS_REG})/!(${FS_SEGMENTS_REG})`, 22 | 23 | /** 24 | * Allow slices with structure grouping 25 | * @example 26 | * 'features/auth/form' // Pass 27 | */ 28 | `**/*(${FS_SLICED_LAYERS_REG})/!(${FS_SEGMENTS_REG})/!(${FS_SEGMENTS_REG})`, 29 | 30 | /** 31 | * Allow not segments import in shared segments 32 | * @example 33 | * 'shared/ui/button' // Pass 34 | */ 35 | `**/*shared/*(${FS_SEGMENTS_REG})/!(${FS_SEGMENTS_REG})`, 36 | 37 | /** 38 | * Allow import from segments in shared 39 | * @example 40 | * 'shared/ui' // Pass 41 | */ 42 | `**/*shared/*(${FS_SEGMENTS_REG})`, 43 | 44 | /** allow global modules */ 45 | `**/node_modules/**`, 46 | 47 | /** 48 | * allow custom shared segments with _prefix 49 | */ 50 | `**/*shared/_*`, 51 | `**/*shared/_*/*` 52 | ], 53 | }], 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /rules/public-api/index.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("."); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser( 9 | configLib.mockImports(cfg), 10 | ), 11 | }); 12 | 13 | describe("Allow publicAPI for shared segments with _prefix:", () => { 14 | it("with _prefix should lint without errors", async () => { 15 | const report = await eslint.lintText(` 16 | import { One } from "shared/_route/one"; 17 | import { Two } from "@shared/_note/two"; 18 | import { Route } from "shared/_route"; 19 | `, 20 | {filePath: "src/app/ui/index.js"}); 21 | assert.strictEqual(report[0].errorCount, 0); 22 | }); 23 | 24 | it("import inner module with _prefix should lint with errors", async () => { 25 | const report = await eslint.lintText(` 26 | import { Five } from "@shared/_note/two/five"; 27 | import { Four } from "shared/_note/three/four"; 28 | import { Route } from "shared/route"; 29 | `, 30 | {filePath: "src/app/ui/index.js"}); 31 | assert.strictEqual(report[0].errorCount, 3); 32 | }); 33 | 34 | it("without prefix should lint with errors", async () => { 35 | const report = await eslint.lintText(` 36 | import { One } from "shared/route/one"; 37 | import { Two } from "@shared/note/two"; 38 | `, 39 | {filePath: "src/app/ui/index.js"}); 40 | assert.strictEqual(report[0].errorCount, 2); 41 | }); 42 | }); 43 | 44 | describe("PublicAPI import boundaries:", () => { 45 | it("Should lint PublicAPI boundaries with errors.", async () => { 46 | const report = await eslint.lintText(` 47 | import { Issues } from "pages/issues/ui"; 48 | import { IssueDetails } from "widgets/issue-details/ui/details"; 49 | import { AuthForm } from "features/auth-form/ui/form"; 50 | import { Button } from "shared/ui/button/button"; 51 | import { saveOrder } from "entities/order/model/actions"; 52 | import { orderModel } from "entities/order/model"; 53 | import { TicketCard } from "@src/entities/ticket/ui"; 54 | import { Ticket } from "@src/entities/ticket/ui.tsx"; 55 | `, 56 | { filePath: "src/app/ui/index.js" }); 57 | assert.strictEqual(report[0].errorCount, 8); 58 | }); 59 | 60 | it("Should lint PublicAPI boundaries without errors.", async () => { 61 | const report = await eslint.lintText(` 62 | import { Issues } from "pages/issues"; 63 | import { GoodIssues } from "@src/pages/issues"; 64 | import { IssueDetails } from "widgets/issue-details"; 65 | import { AuthForm } from "features/auth-form"; 66 | import { Button } from "shared/ui/button"; 67 | import { orderModel } from "entities/order"; 68 | import { TicketCard } from "@/entities/ticket"; 69 | import { FixButton } from "@src/shared/ui/fix-button"; 70 | `, { filePath: "src/app/ui/index.js" }); 71 | assert.strictEqual(report[0].errorCount, 0); 72 | }); 73 | 74 | it("Should lint extra PublicAPI boundaries cases without errors.", async () => { 75 | const report = await eslint.lintText(` 76 | import { AuthForm } from "features/auth/form"; 77 | import { Button } from "shared/ui"; 78 | `, { filePath: "src/app/ui/index.js" }); 79 | 80 | assert.strictEqual(report[0].errorCount, 0); 81 | }); 82 | 83 | describe("Allow not segments import from slices:", () => { 84 | it("should lint without errors", async () => { 85 | const report = await eslint.lintText(` 86 | import { AuthForm } from "entities/auth"; 87 | import { model } from "../model"; 88 | import { styles } from "./styles.module.scss"; 89 | `, { filePath: "src/features/form/ui/index.js" }); 90 | 91 | assert.strictEqual(report[0].errorCount, 0); 92 | }); 93 | 94 | it("should lint with errors", async () => { 95 | const report = await eslint.lintText(` 96 | import { AuthForm } from "entities/auth/ui"; 97 | import { Button } from "shared/button"; 98 | `, { filePath: "src/features/form/ui/index.js" }); 99 | 100 | assert.strictEqual(report[0].errorCount, 2); 101 | }); 102 | }); 103 | 104 | describe("Allow slices with structure grouping:", () => { 105 | it("should lint with errors", async () => { 106 | const report = await eslint.lintText(` 107 | import { AuthForm } from "entities/auth/form"; 108 | `, { filePath: "src/features/form/ui/index.js" }); 109 | 110 | assert.strictEqual(report[0].errorCount, 0); 111 | }); 112 | 113 | it("should lint without errors", async () => { 114 | const report = await eslint.lintText(` 115 | import { AuthForm } from "entities/auth/ui"; 116 | import { Form } from "shared/button/form"; 117 | `, { filePath: "src/features/form/ui/index.js" }); 118 | 119 | assert.strictEqual(report[0].errorCount, 2); 120 | }); 121 | }); 122 | 123 | describe("Allow not segments import in shared segments:", () => { 124 | it("should lint without errors", async () => { 125 | const report = await eslint.lintText(` 126 | import { Form } from "shared/ui/form"; 127 | import { AuthAPI } from "shared/api/auth"; 128 | import { useGeo } from "shared/lib/hooks"; 129 | import { styles } from "shared/ui/styles"; 130 | import { lockSound } from "shared/assets"; 131 | import { CONNECT_ATTEMPTS } from "shared/config"; 132 | `, { filePath: "src/features/form/ui/index.js" }); 133 | 134 | assert.strictEqual(report[0].errorCount, 0); 135 | }); 136 | 137 | it("should lint with errors", async () => { 138 | const report = await eslint.lintText(` 139 | import { Hex } from "shared/api/ui"; 140 | import { Form } from "shared/ui/lib"; 141 | import { AuthForm } from "shared/api/ui"; 142 | import { lockSound } from "shared/assets/ui"; 143 | import { model } from "shared/ui/model"; 144 | `, { filePath: "src/features/form/ui/index.js" }); 145 | 146 | assert.strictEqual(report[0].errorCount, 5); 147 | }); 148 | 149 | it("should lint without errors", async () => { 150 | const report = await eslint.lintText(` 151 | import { FancyLabel } from "../../label"; 152 | import { model } from "../model"; 153 | import { lockSound } from "../assets"; 154 | `, { filePath: "src/shared/ui/button/index.js" }); 155 | 156 | assert.strictEqual(report[0].errorCount, 0); 157 | }); 158 | 159 | it("should lint aliases without errors", async () => { 160 | const report = await eslint.lintText(` 161 | import { routeNames } from '@/entities/one'; 162 | import { fetchRules } from '@entities/two'; 163 | import { Three } from '@features/three'; 164 | import { Four } from '@/features/four'; 165 | `, { filePath: "src/pages/main/ui/index.js" }); 166 | 167 | assert.strictEqual(report[0].errorCount, 0); 168 | }); 169 | }); 170 | 171 | describe("Import from segments in shared:", () => { 172 | it("should lint without errors", async () => { 173 | const report = await eslint.lintText(` 174 | import { AuthAPI } from "shared/api"; 175 | import { FancyLabel } from 'shared/ui'; 176 | import { convertToken } from 'shared/lib'; 177 | import { CONNECT_ATTEMPTS } from "shared/config"; 178 | import { lockSound } from "shared/assets"; 179 | `, { filePath: "src/pages/main/ui/index.js" }); 180 | 181 | assert.strictEqual(report[0].errorCount, 0); 182 | }); 183 | 184 | it("should lint with errors", async () => { 185 | const report = await eslint.lintText(` 186 | import { AuthAPI } from "shared/auth"; 187 | import { FancyLabel } from 'shared/label'; 188 | import { convertToken } from 'shared/token'; 189 | import { CONNECT_ATTEMPTS } from "shared/const"; 190 | `, { filePath: "src/pages/main/ui/index.js" }); 191 | 192 | assert.strictEqual(report[0].errorCount, 4); 193 | }); 194 | 195 | it("should lint shared aliases without errors", async () => { 196 | const report = await eslint.lintText(` 197 | import { routeNames } from '@/shared/api/router'; 198 | import { fetchRules } from '@shared/api/rules'; 199 | import { lockSound } from "shared/assets/sounds"; 200 | `, { filePath: "src/pages/main/ui/index.js" }); 201 | 202 | assert.strictEqual(report[0].errorCount, 0); 203 | }); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /rules/public-api/lite.js: -------------------------------------------------------------------------------- 1 | const { layersLib } = require("../../utils"); 2 | 3 | const FS_SLICED_LAYERS_REG = layersLib.getUpperLayers("shared").join("|"); 4 | const FS_SEGMENTS_REG = [ 5 | ...layersLib.FS_SEGMENTS, 6 | ...layersLib.FS_SEGMENTS.map((seg) => `${seg}.*`), 7 | ].join('|'); 8 | 9 | module.exports = { 10 | plugins: ["import"], 11 | rules: { 12 | "import/no-internal-modules": [ 13 | "error", { 14 | "allow": [ 15 | /** 16 | * Allow not segments import from slices 17 | * @example 18 | * 'entities/form/ui' // Fail 19 | * 'entities/form' // Pass 20 | */ 21 | `**/*(${FS_SLICED_LAYERS_REG})/!(${FS_SEGMENTS_REG})`, 22 | 23 | /** 24 | * Allow slices with structure grouping 25 | * @example 26 | * 'features/auth/form' // Pass 27 | */ 28 | `**/*(${FS_SLICED_LAYERS_REG})/!(${FS_SEGMENTS_REG})/!(${FS_SEGMENTS_REG})`, 29 | 30 | /** 31 | * Allow not segments import in shared segments 32 | * @example 33 | * 'shared/ui/button' // Pass 34 | */ 35 | `**/*shared/*(${FS_SEGMENTS_REG})/!(${FS_SEGMENTS_REG})`, 36 | 37 | /** 38 | * Allow import from segments in shared 39 | * @example 40 | * 'shared/ui' // Pass 41 | */ 42 | `**/*shared/*(${FS_SEGMENTS_REG})`, 43 | 44 | /** allow global modules */ 45 | `**/node_modules/**`, 46 | 47 | /** 48 | * allow custom shared segments with _prefix 49 | */ 50 | `**/*shared/_*`, 51 | `**/*shared/_*/*`, 52 | 53 | /** 54 | * Used for allow import local modules 55 | * @example 56 | * './model/something' // Pass 57 | */ 58 | `./**` 59 | 60 | ], 61 | }], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /rules/public-api/lite.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("./lite"); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser( 9 | configLib.mockImports(cfg), 10 | ), 11 | }); 12 | 13 | describe("Lite PublicAPI:", () => { 14 | it("local segments should be ignored in Lite config.", async () => { 15 | const report = await eslint.lintText(` 16 | export { SubmitButton } from "./ui/button"; 17 | export { SmthForm } from "./ui/form"; 18 | export * from "./model/actions"; 19 | export { selectSmthById } from "./model/selectors"; 20 | `, 21 | { filePath: "src/features/smth/index.ts" }); 22 | assert.strictEqual(report[0].errorCount, 0); 23 | }); 24 | 25 | it("should lint with errors.", async () => { 26 | const report = await eslint.lintText(` 27 | import { Button } from "../shared/ui/button/button"; 28 | import { Date } from "../shared/lib/date/date"; 29 | `, 30 | {filePath: "src/features/index.ts"}); 31 | assert.strictEqual(report[0].errorCount, 2); 32 | }); 33 | 34 | it("should lint without errors when local import in shared layer.", async () => { 35 | const report = await eslint.lintText(` 36 | import { Button } from "./button/button"; 37 | `, 38 | {filePath: "src/shared/ui/index.ts"}); 39 | assert.strictEqual(report[0].errorCount, 0); 40 | }); 41 | 42 | it("should lint with error when layer import in shared layer.", async () => { 43 | const report = await eslint.lintText(` 44 | import { Button } from "shared/ui/button/button"; 45 | `, 46 | {filePath: "src/shared/ui/index.ts"}); 47 | assert.strictEqual(report[0].errorCount, 1); 48 | }); 49 | }) 50 | 51 | describe("Allow publicAPI for shared segments with _prefix:", () => { 52 | it("with _prefix should lint without errors", async () => { 53 | const report = await eslint.lintText(` 54 | import { One } from "shared/_route/one"; 55 | import { Two } from "@shared/_note/two"; 56 | import { Route } from "shared/_route"; 57 | `, 58 | {filePath: "src/app/ui/index.js"}); 59 | assert.strictEqual(report[0].errorCount, 0); 60 | }); 61 | 62 | it("import inner module with _prefix should lint with errors", async () => { 63 | const report = await eslint.lintText(` 64 | import { Five } from "@shared/_note/two/five"; 65 | import { Four } from "shared/_note/three/four"; 66 | import { Route } from "shared/route"; 67 | `, 68 | {filePath: "src/app/ui/index.js"}); 69 | assert.strictEqual(report[0].errorCount, 3); 70 | }); 71 | 72 | it("without prefix should lint with errors", async () => { 73 | const report = await eslint.lintText(` 74 | import { One } from "shared/route/one"; 75 | import { Two } from "@shared/note/two"; 76 | `, 77 | {filePath: "src/app/ui/index.js"}); 78 | assert.strictEqual(report[0].errorCount, 2); 79 | }); 80 | }); 81 | 82 | describe("PublicAPI import boundaries:", () => { 83 | it("Should lint PublicAPI boundaries with errors.", async () => { 84 | const report = await eslint.lintText(` 85 | import { Issues } from "pages/issues/ui"; 86 | import { IssueDetails } from "widgets/issue-details/ui/details"; 87 | import { AuthForm } from "features/auth-form/ui/form"; 88 | import { Button } from "shared/ui/button/button"; 89 | import { saveOrder } from "entities/order/model/actions"; 90 | import { orderModel } from "entities/order/model"; 91 | import { TicketCard } from "@src/entities/ticket/ui"; 92 | import { Ticket } from "@src/entities/ticket/ui.tsx"; 93 | `, 94 | { filePath: "src/app/ui/index.js" }); 95 | assert.strictEqual(report[0].errorCount, 8); 96 | }); 97 | 98 | it("Should lint PublicAPI boundaries without errors.", async () => { 99 | const report = await eslint.lintText(` 100 | import { Issues } from "pages/issues"; 101 | import { GoodIssues } from "@src/pages/issues"; 102 | import { IssueDetails } from "widgets/issue-details"; 103 | import { AuthForm } from "features/auth-form"; 104 | import { Button } from "shared/ui/button"; 105 | import { orderModel } from "entities/order"; 106 | import { TicketCard } from "@/entities/ticket"; 107 | import { FixButton } from "@src/shared/ui/fix-button"; 108 | `, { filePath: "src/app/ui/index.js" }); 109 | assert.strictEqual(report[0].errorCount, 0); 110 | }); 111 | 112 | it("Should lint extra PublicAPI boundaries cases without errors.", async () => { 113 | const report = await eslint.lintText(` 114 | import { AuthForm } from "features/auth/form"; 115 | import { Button } from "shared/ui"; 116 | `, { filePath: "src/app/ui/index.js" }); 117 | 118 | assert.strictEqual(report[0].errorCount, 0); 119 | }); 120 | 121 | describe("Allow not segments import from slices:", () => { 122 | it("should lint without errors", async () => { 123 | const report = await eslint.lintText(` 124 | import { AuthForm } from "entities/auth"; 125 | import { model } from "../model"; 126 | import { styles } from "./styles.module.scss"; 127 | `, { filePath: "src/features/form/ui/index.js" }); 128 | 129 | assert.strictEqual(report[0].errorCount, 0); 130 | }); 131 | 132 | it("should lint with errors", async () => { 133 | const report = await eslint.lintText(` 134 | import { AuthForm } from "entities/auth/ui"; 135 | import { Button } from "shared/button"; 136 | `, { filePath: "src/features/form/ui/index.js" }); 137 | 138 | assert.strictEqual(report[0].errorCount, 2); 139 | }); 140 | }); 141 | 142 | describe("Allow slices with structure grouping:", () => { 143 | it("should lint with errors", async () => { 144 | const report = await eslint.lintText(` 145 | import { AuthForm } from "entities/auth/form"; 146 | `, { filePath: "src/features/form/ui/index.js" }); 147 | 148 | assert.strictEqual(report[0].errorCount, 0); 149 | }); 150 | 151 | it("should lint without errors", async () => { 152 | const report = await eslint.lintText(` 153 | import { AuthForm } from "entities/auth/ui"; 154 | import { Form } from "shared/button/form"; 155 | `, { filePath: "src/features/form/ui/index.js" }); 156 | 157 | assert.strictEqual(report[0].errorCount, 2); 158 | }); 159 | }); 160 | 161 | describe("Allow not segments import in shared segments:", () => { 162 | it("should lint without errors", async () => { 163 | const report = await eslint.lintText(` 164 | import { Form } from "shared/ui/form"; 165 | import { AuthAPI } from "shared/api/auth"; 166 | import { useGeo } from "shared/lib/hooks"; 167 | import { styles } from "shared/ui/styles"; 168 | import { CONNECT_ATTEMPTS } from "shared/config"; 169 | `, { filePath: "src/features/form/ui/index.js" }); 170 | 171 | assert.strictEqual(report[0].errorCount, 0); 172 | }); 173 | 174 | it("should lint with errors", async () => { 175 | const report = await eslint.lintText(` 176 | import { Hex } from "shared/api/ui"; 177 | import { Form } from "shared/ui/lib"; 178 | import { AuthForm } from "shared/api/ui"; 179 | import { model } from "shared/ui/model"; 180 | `, { filePath: "src/features/form/ui/index.js" }); 181 | 182 | assert.strictEqual(report[0].errorCount, 4); 183 | }); 184 | 185 | it("should lint without errors", async () => { 186 | const report = await eslint.lintText(` 187 | import { FancyLabel } from "../../label"; 188 | import { model } from "../model"; 189 | `, { filePath: "src/shared/ui/button/index.js" }); 190 | 191 | assert.strictEqual(report[0].errorCount, 0); 192 | }); 193 | 194 | it("should lint aliases without errors", async () => { 195 | const report = await eslint.lintText(` 196 | import { routeNames } from '@/entities/one'; 197 | import { fetchRules } from '@entities/two'; 198 | import { Three } from '@features/three'; 199 | import { Four } from '@/features/four'; 200 | `, { filePath: "src/pages/main/ui/index.js" }); 201 | 202 | assert.strictEqual(report[0].errorCount, 0); 203 | }); 204 | }); 205 | 206 | describe("Import from segments in shared:", () => { 207 | it("should lint without errors", async () => { 208 | const report = await eslint.lintText(` 209 | import { AuthAPI } from "shared/api"; 210 | import { FancyLabel } from 'shared/ui'; 211 | import { convertToken } from 'shared/lib'; 212 | import { CONNECT_ATTEMPTS } from "shared/config"; 213 | `, { filePath: "src/pages/main/ui/index.js" }); 214 | 215 | assert.strictEqual(report[0].errorCount, 0); 216 | }); 217 | 218 | it("should lint with errors", async () => { 219 | const report = await eslint.lintText(` 220 | import { AuthAPI } from "shared/auth"; 221 | import { FancyLabel } from 'shared/label'; 222 | import { convertToken } from 'shared/token'; 223 | import { CONNECT_ATTEMPTS } from "shared/const"; 224 | `, { filePath: "src/pages/main/ui/index.js" }); 225 | 226 | assert.strictEqual(report[0].errorCount, 4); 227 | }); 228 | 229 | it("should lint shared aliases without errors", async () => { 230 | const report = await eslint.lintText(` 231 | import { routeNames } from '@/shared/api/router'; 232 | import { fetchRules } from '@shared/api/rules'; 233 | `, { filePath: "src/pages/main/ui/index.js" }); 234 | 235 | assert.strictEqual(report[0].errorCount, 0); 236 | }); 237 | }); 238 | 239 | describe("Segment PublicAPI:", () => { 240 | it("Should lint Segment PublicAPI boundaries without errors.", async () => { 241 | const report = await eslint.lintText(` 242 | export { SubmitButton, SmthForm } from "./ui"; 243 | export * from "./model"; 244 | export { selectSmthById, selectSmthByName } from "./model"; 245 | `, { filePath: "src/features/smth/index.ts" }); 246 | 247 | assert.strictEqual(report[0].errorCount, 0); 248 | }); 249 | 250 | describe("Exclusive Segments Public API for shared layer:", () => { 251 | it("Should lint with errors.", async () => { 252 | const report = await eslint.lintText(` 253 | import { Button } from "shared/ui/button/button"; 254 | import { Date } from "shared/lib/date/date"; 255 | `, 256 | { filePath: "src/features/smth/index.ts" }); 257 | assert.strictEqual(report[0].errorCount, 2); 258 | }); 259 | 260 | it("Should lint without errors.", async () => { 261 | const report = await eslint.lintText(` 262 | import { Button } from "shared/ui/button"; 263 | import { SexyButton } from "shared/ui"; 264 | import { Parser } from "shared/lib"; 265 | import { Date } from "shared/lib/date"; 266 | `, { filePath: "src/features/smth/index.ts" }); 267 | 268 | assert.strictEqual(report[0].errorCount, 0); 269 | }); 270 | }); 271 | 272 | }); 273 | 274 | }); 275 | -------------------------------------------------------------------------------- /rules/public-api/segment-public-api.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const { configLib } = require("../../utils"); 4 | const cfg = require("."); 5 | 6 | const eslint = new ESLint({ 7 | useEslintrc: false, 8 | baseConfig: configLib.setParser( 9 | configLib.mockImports(cfg), 10 | ), 11 | }); 12 | 13 | describe("Segment PublicAPI:", () => { 14 | it("Should lint Segment PublicAPI boundaries with errors.", async () => { 15 | const report = await eslint.lintText(` 16 | export { SubmitButton } from "./ui/button"; 17 | export { SmthForm } from "./ui/form"; 18 | export * from "./model/actions"; 19 | export { selectSmthById } from "./model/selectors"; 20 | `, 21 | { filePath: "src/features/smth/index.ts" }); 22 | assert.strictEqual(report[0].errorCount, 4); 23 | }); 24 | 25 | it("Should lint Segment PublicAPI boundaries without errors.", async () => { 26 | const report = await eslint.lintText(` 27 | export { SubmitButton, SmthForm } from "./ui"; 28 | export * from "./model"; 29 | export { selectSmthById, selectSmthByName } from "./model"; 30 | `, { filePath: "src/features/smth/index.ts" }); 31 | 32 | assert.strictEqual(report[0].errorCount, 0); 33 | }); 34 | 35 | describe("Exclusive Segments Public API for shared layer:", () => { 36 | it("Should lint with errors.", async () => { 37 | const report = await eslint.lintText(` 38 | import { Button } from "shared/ui/button/button"; 39 | import { Date } from "shared/lib/date/date"; 40 | `, 41 | { filePath: "src/features/smth/index.ts" }); 42 | assert.strictEqual(report[0].errorCount, 2); 43 | }); 44 | 45 | it("Should lint without errors.", async () => { 46 | const report = await eslint.lintText(` 47 | import { Button } from "shared/ui/button"; 48 | import { SexyButton } from "shared/ui"; 49 | import { Parser } from "shared/lib"; 50 | import { Date } from "shared/lib/date"; 51 | `, { filePath: "src/features/smth/index.ts" }); 52 | 53 | assert.strictEqual(report[0].errorCount, 0); 54 | }); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const { typesLib } = require("../utils"); 3 | const cfg = require("../"); 4 | 5 | describe("config is valid", () => { 6 | it("Valid parerOptions should presented in global config.", () => { 7 | assert.ok(typesLib.isObj(cfg.parserOptions)); 8 | Object.entries(cfg.parserOptions).forEach(([key, value]) => { 9 | assert.ok(typesLib.isString(key)); 10 | assert.ok(typesLib.isString(value)); 11 | }); 12 | }); 13 | 14 | it("Global config should extends other config.", () => { 15 | assert.ok(typesLib.isArray(cfg.extends)); 16 | }); 17 | 18 | it("All extended configs should be presented.", () => { 19 | cfg.extends.forEach((configPath) => { 20 | const config = require(configPath); 21 | assert.ok(config); 22 | }); 23 | }); 24 | 25 | it("All extended configs plugins should be presented as Array's", () => { 26 | cfg.extends.forEach((configPath) => { 27 | const config = require(configPath); 28 | assert.ok(typesLib.isArray(config.plugins)); 29 | }); 30 | }); 31 | 32 | it("All extended configs rules should be with name and value", () => { 33 | cfg.extends.forEach((configPath) => { 34 | const config = require(configPath); 35 | assert.ok(typesLib.isObj(config.rules)); 36 | Object.entries(config.rules).forEach(([ruleName, ruleOptions]) => { 37 | assert.ok(typesLib.isString(ruleName)); 38 | assert.ok( 39 | typesLib.isNumber(ruleOptions) || 40 | typesLib.isArray(ruleOptions) || 41 | typesLib.isObj(ruleOptions), 42 | ); 43 | }); 44 | }); 45 | }); 46 | // TODO: eslint.isValid 47 | // NOTE: maybe will need... in future... 48 | // t.ok(isObject(config.parserOptions)) 49 | // t.ok(isObject(config.env)) 50 | // t.ok(isObject(config.globals)) 51 | // t.ok(isObject(config.rules)) 52 | }); 53 | -------------------------------------------------------------------------------- /test/lint.test.js: -------------------------------------------------------------------------------- 1 | const { ESLint } = require("eslint"); 2 | const assert = require("assert"); 3 | const cfg = require(".."); 4 | 5 | const eslint = new ESLint({ 6 | useEslintrc: false, 7 | baseConfig: cfg, 8 | }); 9 | 10 | // Should be actualized (https://github.com/feature-sliced/eslint-config/issues/17) 11 | describe.skip("restrict imports", () => { 12 | it("invalid", async () => { 13 | const report = await eslint.lintText(` 14 | import { Issues } from "pages/issues"; 15 | import { IssueDetails } from "features/issue-details" 16 | import { Button } from "shared/components/button"; 17 | `); 18 | assert.strictEqual(report[0].errorCount, 3); 19 | }); 20 | it("valid", async () => { 21 | const report = await eslint.lintText(` 22 | import Routing from "pages"; // specific pages shouldn't be reexported 23 | import { IssueDetails } from "features" // all features should be reexported, for usage 24 | import { Button } from "shared/components"; // all components should be reexported, for usage 25 | `); 26 | assert.strictEqual(report[0].errorCount, 0); 27 | }); 28 | }); 29 | 30 | // Should be actualized (https://github.com/feature-sliced/eslint-config/issues/17) 31 | describe.skip("absolute imports", () => { 32 | it("invalid", async () => { 33 | const report = await eslint.lintText(` 34 | import Routing from "../../pages" 35 | import { IssueDetails } from "../features"; 36 | import { Button } from "../shared/components"; 37 | `); 38 | assert.strictEqual(report[0].errorCount, 3); 39 | }); 40 | it("valid", async () => { 41 | const report = await eslint.lintText(` 42 | import Routing from "pages" 43 | import { IssueDetails } from "features"; 44 | import { Button } from "shared/components"; 45 | `); 46 | assert.strictEqual(report[0].errorCount, 0); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /utils/config/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const mockImports = (config, extension = "js") => { 4 | return { 5 | ...config, 6 | settings: { 7 | ...config.settings, 8 | "import/resolver": { 9 | [path.resolve(__dirname, "./mock-resolver.js")]: { 10 | extension, 11 | }, 12 | }, 13 | } 14 | } 15 | } 16 | 17 | function setParser (config, version = "2015") { 18 | return { 19 | ...config, 20 | parserOptions: { 21 | "ecmaVersion": version, 22 | "sourceType": "module", 23 | }, 24 | }; 25 | } 26 | 27 | function setTSParser (config) { 28 | return { 29 | ...config, 30 | parser: "@typescript-eslint/parser", 31 | }; 32 | } 33 | 34 | module.exports.configLib = { mockImports, setParser, setTSParser }; 35 | -------------------------------------------------------------------------------- /utils/config/mock-resolver.js: -------------------------------------------------------------------------------- 1 | /* Used by import/boundaries plugin for configure parser version */ 2 | const interfaceVersion = 2; 3 | 4 | function resolve (source, file, settings) { 5 | return { found: true, path: `${source}/index.${settings.extension}` }; 6 | } 7 | 8 | module.exports = { interfaceVersion, resolve }; 9 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | const { configLib } = require("./config"); 2 | const { layersLib } = require("./layers"); 3 | const { typesLib } = require("./types"); 4 | 5 | module.exports = { 6 | configLib, 7 | layersLib, 8 | typesLib, 9 | }; 10 | -------------------------------------------------------------------------------- /utils/layers.js: -------------------------------------------------------------------------------- 1 | const FS_LAYERS = [ 2 | "app", 3 | "processes", 4 | "pages", 5 | "widgets", 6 | "features", 7 | "entities", 8 | "shared", 9 | ]; 10 | 11 | const FS_SEGMENTS = [ 12 | "ui", 13 | "model", 14 | "lib", 15 | "api", 16 | "config", 17 | "assets" 18 | ]; 19 | 20 | const getLowerLayers = (layer) => FS_LAYERS.slice(FS_LAYERS.indexOf(layer) + 1); 21 | const getUpperLayers = (layer) => FS_LAYERS.slice(0, FS_LAYERS.indexOf(layer)); 22 | 23 | module.exports.layersLib = { FS_LAYERS, FS_SEGMENTS, getLowerLayers, getUpperLayers } ; 24 | -------------------------------------------------------------------------------- /utils/types.js: -------------------------------------------------------------------------------- 1 | const isArray = (val) => Array.isArray(val); 2 | const isObj = (val) => typeof val === "object" && val !== null; 3 | const isString = (val) => typeof val === "string"; 4 | const isNumber = (val) => typeof val === "number"; 5 | const isOptional = (val) => val === undefined; 6 | 7 | module.exports.typesLib = { 8 | isArray, 9 | isObj, 10 | isString, 11 | isNumber, 12 | isOptional, 13 | }; 14 | --------------------------------------------------------------------------------