npm package: @ms-cloudpack/eslint-plugin
Description: A set of ESLint rules for Cloudpack
Latest version: 0.6.2

# @ms-cloudpack/eslint-plugin

`@ms-cloudpack/eslint-plugin` provides a shared config and custom rules to help encourage Cloudpack-friendly coding practices, specifically regarding imports and exports.

## Included configs

This plugin includes the following configs:

- `recommended`: Includes rules that are recommended for helping Cloudpack work better in your repo. None of these rules require type checking.
  - [`@ms-cloudpack/no-unsupported-imports`](#no-unsupported-imports)
  - [`@rnx-kit/no-const-enum`](https://www.npmjs.com/package/@rnx-kit/eslint-plugin)
  - [`@rnx-kit/no-export-all`](https://www.npmjs.com/package/@rnx-kit/eslint-plugin)
- `recommended-requiring-type-checking`: Includes additional rules which are helpful for Cloudpack but require type information.
  - All `recommended` rules
  - [`@typescript-eslint/consistent-type-exports`](https://typescript-eslint.io/rules/consistent-type-exports)
  - [`@typescript-eslint/consistent-type-imports`](https://typescript-eslint.io/rules/consistent-type-imports)
  - [`@typescript-eslint/no-import-type-side-effects`](https://typescript-eslint.io/rules/no-import-type-side-effects)
- `onboarding`: Includes the same rules as `recommended`, but at warn level.
- `onboarding-requiring-type-checking`: As above.

### Using a config

1. Install required peers: `@rnx-kit/eslint-plugin @typescript-eslint/eslint-plugin @typescript-eslint/parser typescript`
   - For flat config, also install `typescript-eslint`
1. If using a `*-requiring-type-checking` config, follow the instructions for [linting with type information](https://typescript-eslint.io/linting/typed-linting).
1. Add to your config:
   - Classic:
     ```json
     {
       "plugins": ["@ms-cloudpack"],
       "extends": ["plugin:@ms-cloudpack/config-name-here"]
     }
     ```
   - Flat:

     ```js
     import tseslint from 'typescript-eslint';
     // Note the "/flat" path (configs at the package root are classic only)
     import cloudpack from '@ms-cloudpack/eslint-plugin/flat';

     export default tseslint.config(cloudpack.configs['config-name-here']);
     ```

## Included rules

- These rules do not require type checking
- ✓: Enabled with `@ms-cloudpack/recommended` config

|  ✓  | Rule                                   | Description                                               |
| :-: | :------------------------------------- | :-------------------------------------------------------- |
|     | `@ms-cloudpack/no-test-exports`        | Ban test, mock, and fixture exports from index files      |
|  ✓  | `@ms-cloudpack/no-unsupported-imports` | Ban deep imports that are not defined in an `exports` map |

### Enabling rules

1. Install required peers: `@typescript-eslint/eslint-plugin @typescript-eslint/parser typescript`
   - For flat config, also install `typescript-eslint`
2. Reference the plugin and enable the rules:
   - Classic:
     ```json
     {
       "plugins": ["@ms-cloudpack"],
       "rules": {
         "@ms-cloudpack/rule-name-here": "error"
       }
     }
     ```
   - Flat:

     ```js
     import tseslint from 'typescript-eslint';
     // If you're only enabling @ms-cloudpack rules (not extending a config),
     // import from the package root to avoid imports of optional peers.
     import cloudpack from '@ms-cloudpack/eslint-plugin';
     // If you're also extending a config, import from here instead:
     // import cloudpack from '@ms-cloudpack/eslint-plugin/flat';

     export default tseslint.config({
       plugins: {
         // Omit if extending a config (the configs already include this)
         '@ms-cloudpack': cloudpack,
       },
       rules: {
         '@ms-cloudpack/rule-name-here': 'error',
       },
     });
     ```

### `no-test-exports`

This rule bans exporting paths or names which appear to be test-related from index files (unless the index file is also under a test-related path).

Note that this rule checks the filename being linted and will **only** run on `index.*` files. Files under paths that appear test-related (based on the `testPathPatterns` setting) will be ignored. If you have the rule enabled for a whole repo, you'll need to disable it for any test-related packages.

Mixing test and "production" exports in a single package entry point is not recommended for several reasons:

- Especially if the package is published, it's not clear to consumers what should actually be considered part of the main public API.
- For Cloudpack, it will cause traversal of additional test-related dependencies such as `jest` and its dependency tree. These packages are often not intended for use in the browser and may fail to bundle, which causes time to be wasted debugging issues that won't be relevant at runtime.
- Even for other bundlers such as Webpack (or any tool that traverses code), traversing into test dependency code can cause a parse time penalty even if the exports are ultimately tree-shaken out.

The rule is unfortunately not auto-fixable because ESLint is designed to operate on single files. The fix is as follows:

- At the package source root, create a new file with a name such as `index.mock.ts` or `index.test.ts` and move all test, mock, and fixture exports into that file.
- If the packages uses an exports map, add this path to the exports map.
- Update consumers to import from the new path, e.g. `my-pkg/index.mock`.

#### Options

For any of the array options, a provided array overwrites the defaults. Include `...` in your custom array to also include the defaults.

- `testPathPatterns` (`string[]`): Ban importing or exporting from paths matching these regular expressions in index files. Also, index files underneath matching paths will be ignored. Default as of writing:
  - File or directory starting with `mock|test|fixture` (optionally with `__` before)
  - File or directory containing `Mock|Test|Fixture`
  - File ending with `.(mock|test|fixture)` (optionally with extension)
- `testExportNamePatterns` (`string[]`): Ban exporting identifiers matching these regular expressions in index files. Default as of writing:
  - Name starting with `mock|test|fixture`
  - Name containing `Mock|Test|Fixture`
- `maxDepth` (`number`): Maximum depth of index files to check relative to the package root (zero-indexed). By default this is set to infinity in case of re-exports. Set to 0 to only check `<package root>/index.ts` or 1 to also check `src/index.ts`.

#### Examples

These examples applies to any `index.*` file which is within the configured `maxDepth` from the package root:

```js
// ✅ OK
export { foo } from './foo';
// With testPathPatterns: []
export { foo } from './mocks/foo';
// With testExportNamePatterns: []
export { mockFoo } from './foo';

// ❌ Error
// These examples use "mock" to be concise, but the same rules apply by default to "test" and "fixture"
export { foo } from './foo.mock';
export { foo } from './mock-foo';
export { foo } from './mocks/foo';
export { foo } from './__mocks__/foo';
export * as foo from './foo.mock';
export { mockFoo, bar as mockBar } from './foo';
export const mockFoo = 'foo';
export { fooMock };
// Importing from test-related files in an index file is also banned
import { foo } from './foo.mock';
```

### `no-unsupported-imports`

This rule bans deep imports from paths which aren't defined in a package's [exports map](https://nodejs.org/api/packages.html#package-entry-points). It also bans **all** deep imports from packages which don't define exports maps.

Features:

- Checks all common "import-like" syntax variants (`import`, `export` from paths, `require`)
- Basic support for wildcards and directories in exports maps
- Caches resolved package info to reduce disk access
- Provides suggestions (not auto-fixes) for importing from package root in some cases

Current limitations (handling for some of these could be added if needed):

- Not sensitive to conditions: it passes if a path is exported for any condition
- Can't auto-fix. This would require either type information (to figure out where symbols are defined and whether they're exported from the root) or complete manual traversal of a package's exported paths and symbols.
- Limited handling of later exclusions underneath wildcards or directories
- Limited support for checking template strings in `import()` or `require()` (non-literal strings are ignored)
- Doesn't verify that the resolved path exists on disk (reduces cost and complexity)
- Does no verification of top-level package imports. This is intentional for performance and usually correct, but it would fail to detect a package that _only_ defines deep imports in its exports map. (Importing specific packages with this issue could be banned with ESLint's `no-restricted-imports`.)

#### Options

- `ignorePatterns` (`string[]`): Don't check imports matching these regular expression patterns. Examples:
  - `"^foo/"`: Ignore all imports under `foo`
  - `"^foo/bar/`: Ignore imports under `foo/bar`
  - `"^foo/bar/baz$"`: Ignore the specific path `foo/bar/baz`

#### Examples

For this package without an exports map:

```json
{
  "name": "no-exports",
  "main": "lib/index.js"
}
```

The rule bans all deep imports, regardless of whether the files being imported exist on disk:

```js
// ✅ OK
import { foo } from 'no-exports';
// and other variants of import/export syntax

// ❌ Error
import { foo } from 'no-exports/lib/foo';
import { foo } from 'no-exports/lib/index';
const packageJson = require('no-exports/package.json');

// ✅ OK with ignorePatterns such as ["^no-exports/"] or ["^no-exports/lib/foo$"]
import { foo } from 'no-exports/lib/foo';
```

For this package with an exports map:

```json
{
  "name": "with-exports",
  "main": "lib/index.js",
  "exports": {
    ".": "./lib/index.js",
    "./lib/foo": "./lib/foo.js",
    "./lib/utils/*": "./lib/utils/*.js"
  }
}
```

The rule allows root imports or those matching keys in the exports map:

```js
// ✅ OK
import { foo } from 'with-exports';
import { foo } from 'with-exports/lib/foo';
import { foo } from 'with-exports/lib/utils/bar';

// ❌ Error
// Not included in exports
import { foo } from 'with-exports/lib/nope';
const packageJson = require('with-exports/package.json');
// Path in exports doesn't have extension
import { foo } from 'with-exports/lib/foo.js';

// ✅ OK with ignorePatterns such as ["^with-exports/"] or ["^with-exports/lib/nope$"]
import { foo } from 'with-exports/lib/nope';
```