@travetto/eslint
Version:
ES Linting Rules
184 lines (145 loc) • 6.66 kB
Markdown
<!-- This file was generated by @travetto/doc and should not be modified directly -->
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/eslint/DOC.tsx and execute "npx trv doc" to rebuild -->
**Install: @travetto/eslint**
```bash
npm install @travetto/eslint
yarn add @travetto/eslint
```
[](https://eslint.org/) is the standard for linting [Typescript](https://typescriptlang.org) and [Javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) code. This module provides some standard linting patterns and the ability to create custom rules. Due to the fact that the framework supports both [CommonJS](https://nodejs.org/api/modules.html) and [Ecmascript Module](https://nodejs.org/api/esm.html) formats, a novel solution was required to allow [ESLint](https://eslint.org/) to load [Ecmascript Module](https://nodejs.org/api/esm.html) files.
**Note**: The [ESLint](https://eslint.org/) has introduced [a new configuration format](https://eslint.org/blog/2022/08/new-config-system-part-3/) which allows for [Ecmascript Module](https://nodejs.org/api/esm.html) files.
In a new project, the first thing that will need to be done, post installation, is to create the eslint configuration file.
**Terminal: Registering the Configuration**
```bash
$ trv lint:register
Wrote eslint config to <workspace-root>/eslint.config.cjs
```
This is the file the linter will use, and any other tooling (e.g. IDEs).
**Code: Sample configuration**
```javascript
process.env.TRV_MANIFEST = './.trv/output/node_modules/@travetto/mono-repo';
const { buildConfig } = require('./.trv/output/node_modules/@travetto/eslint/support/bin/eslint-config.js');
const { RuntimeIndex } = require('./.trv/output/node_modules/@travetto/runtime/__index__.js');
const pluginFiles = RuntimeIndex.find({ folder: f => f === 'support', file: f => /support\/eslint[.]/.test(f.relativeFile) });
const plugins = pluginFiles.map(x => require(x.outputFile));
const config = buildConfig(plugins);
module.exports = config;
```
The output is tied to whether or not you are using the [CommonJS](https://nodejs.org/api/modules.html) or [Ecmascript Module](https://nodejs.org/api/esm.html) format.
Once installed, using the linter is as simple as invoking it via the cli:
**Terminal: Running the Linter**
```bash
npx trv lint
```
Or pointing your IDE to reference the registered configuration file.
It can be seen in the sample configuration, that the configuration is looking for files with the pattern of `support/eslint/.*`
These files will follow a given pattern of:
**Code: Custom Rule Shape**
```typescript
import type eslint from 'eslint';
export type TrvEslintPlugin = {
name: string;
rules: Record<string, {
defaultLevel?: string | boolean | number;
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener;
}>;
};
```
An example plugin is used in the [Travetto](https://travetto.dev) framework for enforcing import patterns:
**Code: Import Order Rule**
```typescript
import type eslint from 'eslint';
import { Program, BaseExpression, Expression } from 'estree';
import type { TrvEslintPlugin } from '@travetto/eslint';
const groupTypeMap = {
node: ['node', 'travetto', 'workspace'],
travetto: ['travetto', 'workspace'],
workspace: ['workspace'],
};
interface TSAsExpression extends BaseExpression {
type: 'TSAsExpression';
expression: Expression;
}
declare module 'estree' {
interface ExpressionMap {
TSAsExpression: TSAsExpression;
}
}
export const ImportOrder: TrvEslintPlugin = {
name: '@travetto-import',
rules: {
order: {
defaultLevel: 'error',
create(context) {
function check({ body }: Program): void {
let groupType: (keyof typeof groupTypeMap) | undefined;
let groupSize = 0;
let contiguous = false;
let prev: eslint.AST.Program['body'][number] | undefined;
for (const node of body) {
let from: string | undefined;
if (node.type === 'ImportDeclaration') {
if (node.source?.value && typeof node.source.value === 'string') {
from = node.source.value;
}
} else if (node.type === 'VariableDeclaration' && node.kind === 'const') {
const [decl] = node.declarations;
let call: Expression | undefined;
const initType = decl?.init?.type;
if (initType === 'CallExpression') {
call = decl.init;
} else if (initType === 'TSAsExpression') { // tslint support
call = decl.init.expression;
}
if (
call?.type === 'CallExpression' && call.callee.type === 'Identifier' &&
call.callee.name === 'require' && call.arguments[0].type === 'Literal'
) {
const arg1 = call.arguments[0];
if (arg1.value && typeof arg1.value === 'string') {
from = arg1.value;
}
}
}
if (!from) {
continue;
}
const lineType: typeof groupType = /^@travetto/.test(from) ? 'travetto' : /^[^.]/.test(from) ? 'node' : 'workspace';
if (/module\/[^/]+\/doc\//.test(context.filename) && lineType === 'workspace' && from.startsWith('..')) {
context.report({ message: 'Doc does not support parent imports', node });
}
if (groupType && !groupTypeMap[groupType].includes(lineType)) {
context.report({ message: `Invalid transition from ${groupType} to ${lineType}`, node });
}
if (groupType === lineType) {
groupSize += 1;
} else if (((node.loc?.end.line ?? 0) - (prev?.loc?.end.line ?? 0)) > 1) {
// Newlines
contiguous = false;
groupSize = 0;
}
if (groupSize === 0) { // New group, who dis
groupSize = 1;
groupType = lineType;
} else if (groupType === lineType && !contiguous) { // Contiguous same
// Do nothing
} else if (groupSize === 1) { // Contiguous diff, count 1
contiguous = true;
groupType = lineType;
} else { // Contiguous diff, count > 1
context.report({ message: `Invalid contiguous groups ${groupType} and ${lineType}`, node });
}
prev = node;
}
}
return { Program: check };
}
}
}
};
```