eslint-plugin-tyrant 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # eslint-plugin-tyrant
2
+
3
+ Pure TypeScript ESLint plugin with a rule that requires a TSDoc file comment at the top of each TypeScript module.
4
+
5
+ ## Rule
6
+
7
+ - `tyrant/require-file-tsdoc`: require every `.ts`, `.tsx`, `.mts`, and `.cts` file to begin with a `/** ... */` TSDoc comment.
8
+ - Accepted preamble before that TSDoc: UTF-8 BOM, a single shebang line, and optional empty lines after the shebang.
9
+ - `tyrant/require-empty-line-after-file-tsdoc`: require at least one empty line after the top-level file TSDoc comment.
10
+ - `tyrant/require-tsdoc-style-comments-before-exports`: if a comment block exists immediately before an export, every comment in that block must use `/** ... */` TSDoc style.
11
+ - This rule is not auto-fixable. If a file does not need module-level docs, disable the rule in that file explicitly.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import tyrant from "eslint-plugin-tyrant";
17
+
18
+ export default [
19
+ {
20
+ files: ["**/*.{ts,tsx,mts,cts}"],
21
+ plugins: { tyrant },
22
+ rules: {
23
+ "tyrant/require-file-tsdoc": "error"
24
+ }
25
+ }
26
+ ];
27
+ ```
@@ -0,0 +1,105 @@
1
+ export declare const rules: {
2
+ readonly "require-empty-line-after-file-tsdoc": import("eslint").Rule.RuleModule;
3
+ readonly "require-file-tsdoc": import("eslint").Rule.RuleModule;
4
+ readonly "require-tsdoc-style-comments-before-exports": import("eslint").Rule.RuleModule;
5
+ };
6
+ export declare const configs: {
7
+ recommended: {
8
+ files: string[];
9
+ plugins: {
10
+ tyrant: {
11
+ meta: {
12
+ readonly name: "eslint-plugin-tyrant";
13
+ readonly version: "0.1.0";
14
+ };
15
+ rules: {
16
+ readonly "require-empty-line-after-file-tsdoc": import("eslint").Rule.RuleModule;
17
+ readonly "require-file-tsdoc": import("eslint").Rule.RuleModule;
18
+ readonly "require-tsdoc-style-comments-before-exports": import("eslint").Rule.RuleModule;
19
+ };
20
+ };
21
+ };
22
+ rules: {
23
+ "tyrant/require-empty-line-after-file-tsdoc": "error";
24
+ "tyrant/require-file-tsdoc": "error";
25
+ "tyrant/require-tsdoc-style-comments-before-exports": "error";
26
+ };
27
+ }[];
28
+ recommendedTypeScript: {
29
+ files: string[];
30
+ plugins: {
31
+ tyrant: {
32
+ meta: {
33
+ readonly name: "eslint-plugin-tyrant";
34
+ readonly version: "0.1.0";
35
+ };
36
+ rules: {
37
+ readonly "require-empty-line-after-file-tsdoc": import("eslint").Rule.RuleModule;
38
+ readonly "require-file-tsdoc": import("eslint").Rule.RuleModule;
39
+ readonly "require-tsdoc-style-comments-before-exports": import("eslint").Rule.RuleModule;
40
+ };
41
+ };
42
+ };
43
+ rules: {
44
+ "tyrant/require-empty-line-after-file-tsdoc": "error";
45
+ "tyrant/require-file-tsdoc": "error";
46
+ "tyrant/require-tsdoc-style-comments-before-exports": "error";
47
+ };
48
+ }[];
49
+ };
50
+ declare const plugin: {
51
+ meta: {
52
+ readonly name: "eslint-plugin-tyrant";
53
+ readonly version: "0.1.0";
54
+ };
55
+ rules: {
56
+ readonly "require-empty-line-after-file-tsdoc": import("eslint").Rule.RuleModule;
57
+ readonly "require-file-tsdoc": import("eslint").Rule.RuleModule;
58
+ readonly "require-tsdoc-style-comments-before-exports": import("eslint").Rule.RuleModule;
59
+ };
60
+ configs: {
61
+ recommended: {
62
+ files: string[];
63
+ plugins: {
64
+ tyrant: {
65
+ meta: {
66
+ readonly name: "eslint-plugin-tyrant";
67
+ readonly version: "0.1.0";
68
+ };
69
+ rules: {
70
+ readonly "require-empty-line-after-file-tsdoc": import("eslint").Rule.RuleModule;
71
+ readonly "require-file-tsdoc": import("eslint").Rule.RuleModule;
72
+ readonly "require-tsdoc-style-comments-before-exports": import("eslint").Rule.RuleModule;
73
+ };
74
+ };
75
+ };
76
+ rules: {
77
+ "tyrant/require-empty-line-after-file-tsdoc": "error";
78
+ "tyrant/require-file-tsdoc": "error";
79
+ "tyrant/require-tsdoc-style-comments-before-exports": "error";
80
+ };
81
+ }[];
82
+ recommendedTypeScript: {
83
+ files: string[];
84
+ plugins: {
85
+ tyrant: {
86
+ meta: {
87
+ readonly name: "eslint-plugin-tyrant";
88
+ readonly version: "0.1.0";
89
+ };
90
+ rules: {
91
+ readonly "require-empty-line-after-file-tsdoc": import("eslint").Rule.RuleModule;
92
+ readonly "require-file-tsdoc": import("eslint").Rule.RuleModule;
93
+ readonly "require-tsdoc-style-comments-before-exports": import("eslint").Rule.RuleModule;
94
+ };
95
+ };
96
+ };
97
+ rules: {
98
+ "tyrant/require-empty-line-after-file-tsdoc": "error";
99
+ "tyrant/require-file-tsdoc": "error";
100
+ "tyrant/require-tsdoc-style-comments-before-exports": "error";
101
+ };
102
+ }[];
103
+ };
104
+ };
105
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import { requireEmptyLineAfterFileTSDocRule } from "./rules/require-empty-line-after-file-tsdoc.js";
2
+ import { requireFileTSDocRule } from "./rules/require-file-tsdoc.js";
3
+ import { requireTSDocStyleCommentsBeforeExportsRule } from "./rules/require-tsdoc-style-comments-before-exports.js";
4
+ export const rules = {
5
+ "require-empty-line-after-file-tsdoc": requireEmptyLineAfterFileTSDocRule,
6
+ "require-file-tsdoc": requireFileTSDocRule,
7
+ "require-tsdoc-style-comments-before-exports": requireTSDocStyleCommentsBeforeExportsRule
8
+ };
9
+ const pluginMeta = {
10
+ name: "eslint-plugin-tyrant",
11
+ version: "0.1.0"
12
+ };
13
+ const pluginObject = {
14
+ meta: pluginMeta,
15
+ rules
16
+ };
17
+ export const configs = {
18
+ recommended: [
19
+ {
20
+ files: ["**/*.{ts,tsx,mts,cts}"],
21
+ plugins: {
22
+ tyrant: pluginObject
23
+ },
24
+ rules: {
25
+ "tyrant/require-empty-line-after-file-tsdoc": "error",
26
+ "tyrant/require-file-tsdoc": "error",
27
+ "tyrant/require-tsdoc-style-comments-before-exports": "error"
28
+ }
29
+ }
30
+ ],
31
+ recommendedTypeScript: [
32
+ {
33
+ files: ["**/*.{ts,tsx,mts,cts}"],
34
+ plugins: {
35
+ tyrant: pluginObject
36
+ },
37
+ rules: {
38
+ "tyrant/require-empty-line-after-file-tsdoc": "error",
39
+ "tyrant/require-file-tsdoc": "error",
40
+ "tyrant/require-tsdoc-style-comments-before-exports": "error"
41
+ }
42
+ }
43
+ ]
44
+ };
45
+ const plugin = {
46
+ meta: pluginMeta,
47
+ rules,
48
+ configs
49
+ };
50
+ export default plugin;
@@ -0,0 +1,7 @@
1
+ import type { SourceCode } from "eslint";
2
+ export interface FileTSDocComment {
3
+ end: number;
4
+ start: number;
5
+ }
6
+ export declare function isTypeScriptFilename(filename: string): boolean;
7
+ export declare function getTopLevelTSDocComment(sourceCode: Readonly<SourceCode>): FileTSDocComment | null;
@@ -0,0 +1,33 @@
1
+ const TS_FILE_PATTERN = /\.(?:[cm]?ts|tsx)$/i;
2
+ export function isTypeScriptFilename(filename) {
3
+ return filename !== "<input>" && TS_FILE_PATTERN.test(filename);
4
+ }
5
+ function getFileBodyStart(text) {
6
+ let index = text.charCodeAt(0) === 0xfeff ? 1 : 0;
7
+ if (text.startsWith("#!", index)) {
8
+ const shebangEnd = text.indexOf("\n", index);
9
+ if (shebangEnd === -1) {
10
+ return text.length;
11
+ }
12
+ index = shebangEnd + 1;
13
+ }
14
+ while (index < text.length && text.slice(index, index + 1).trim() === "") {
15
+ index += 1;
16
+ }
17
+ return index;
18
+ }
19
+ export function getTopLevelTSDocComment(sourceCode) {
20
+ const text = sourceCode.getText();
21
+ const expectedStart = getFileBodyStart(text);
22
+ const firstComment = sourceCode
23
+ .getAllComments()
24
+ .find((comment) => comment.range?.[0] !== 0 || !text.startsWith("#!"));
25
+ const [start, end] = firstComment?.range ?? [];
26
+ if (!firstComment || start === undefined || end === undefined || start !== expectedStart) {
27
+ return null;
28
+ }
29
+ if (firstComment.type !== "Block" || !text.slice(start, end).startsWith("/**")) {
30
+ return null;
31
+ }
32
+ return { end, start };
33
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const requireEmptyLineAfterFileTSDocRule: Rule.RuleModule;
@@ -0,0 +1,37 @@
1
+ import { getTopLevelTSDocComment, isTypeScriptFilename } from "./file-tsdoc-utils.js";
2
+ const MESSAGE_ID = "missingEmptyLineAfterFileTSDoc";
3
+ export const requireEmptyLineAfterFileTSDocRule = {
4
+ meta: {
5
+ type: "layout",
6
+ docs: {
7
+ description: "Require an empty line after the top-level file TSDoc comment."
8
+ },
9
+ hasSuggestions: false,
10
+ schema: [],
11
+ messages: {
12
+ [MESSAGE_ID]: "There should be at least one empty line after the top-level file TSDoc comment."
13
+ }
14
+ },
15
+ create(context) {
16
+ if (!isTypeScriptFilename(context.filename)) {
17
+ return {};
18
+ }
19
+ return {
20
+ Program(node) {
21
+ const fileTSDocComment = getTopLevelTSDocComment(context.sourceCode);
22
+ if (!fileTSDocComment) {
23
+ return;
24
+ }
25
+ const textAfterComment = context.sourceCode.getText().slice(fileTSDocComment.end);
26
+ if (textAfterComment.startsWith("\n\n") || textAfterComment.startsWith("\r\n\r\n")) {
27
+ return;
28
+ }
29
+ context.report({
30
+ node,
31
+ loc: context.sourceCode.getLocFromIndex(fileTSDocComment.end),
32
+ messageId: MESSAGE_ID
33
+ });
34
+ }
35
+ };
36
+ }
37
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const requireFileTSDocRule: Rule.RuleModule;
@@ -0,0 +1,33 @@
1
+ import { getTopLevelTSDocComment, isTypeScriptFilename } from "./file-tsdoc-utils.js";
2
+ const MESSAGE_ID = "missingFileTSDoc";
3
+ export const requireFileTSDocRule = {
4
+ meta: {
5
+ type: "suggestion",
6
+ docs: {
7
+ description: "Require a TSDoc block comment at the start of every TypeScript file."
8
+ },
9
+ hasSuggestions: false,
10
+ schema: [],
11
+ messages: {
12
+ [MESSAGE_ID]: "Missing a top-level /** ... */ TSDoc block as a file comment. You can disable this rule in the file if it is not necessary."
13
+ }
14
+ },
15
+ create(context) {
16
+ const filename = context.filename;
17
+ if (!isTypeScriptFilename(filename)) {
18
+ return {};
19
+ }
20
+ return {
21
+ Program(node) {
22
+ if (getTopLevelTSDocComment(context.sourceCode)) {
23
+ return;
24
+ }
25
+ context.report({
26
+ node,
27
+ loc: { line: 1, column: 0 },
28
+ messageId: MESSAGE_ID
29
+ });
30
+ }
31
+ };
32
+ }
33
+ };
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "eslint";
2
+ export declare const requireTSDocStyleCommentsBeforeExportsRule: Rule.RuleModule;
@@ -0,0 +1,92 @@
1
+ import { isTypeScriptFilename } from "./file-tsdoc-utils.js";
2
+ const MESSAGE_ID = "nonTSDocCommentBeforeExport";
3
+ function hasOnlyWhitespaceBetween(sourceCode, start, end) {
4
+ return sourceCode.getText().slice(start, end).trim() === "";
5
+ }
6
+ function getLeadingCommentBlock(sourceCode, node) {
7
+ if (!node.range) {
8
+ return [];
9
+ }
10
+ const comments = sourceCode.getCommentsBefore(node);
11
+ if (comments.length === 0) {
12
+ return [];
13
+ }
14
+ const relevantComments = [];
15
+ let nextStart = node.range[0];
16
+ for (let index = comments.length - 1; index >= 0; index -= 1) {
17
+ const comment = comments[index];
18
+ const [start, end] = comment.range ?? [];
19
+ if (start === undefined || end === undefined) {
20
+ continue;
21
+ }
22
+ if (!hasOnlyWhitespaceBetween(sourceCode, end, nextStart)) {
23
+ break;
24
+ }
25
+ relevantComments.unshift(comment);
26
+ nextStart = start;
27
+ }
28
+ return relevantComments;
29
+ }
30
+ function isTSDocStyleComment(sourceCode, comment) {
31
+ const [start, end] = comment.range ?? [];
32
+ if (start === undefined || end === undefined) {
33
+ return false;
34
+ }
35
+ if (comment.type !== "Block") {
36
+ return false;
37
+ }
38
+ return sourceCode.getText().slice(start, end).startsWith("/**");
39
+ }
40
+ export const requireTSDocStyleCommentsBeforeExportsRule = {
41
+ meta: {
42
+ type: "suggestion",
43
+ docs: {
44
+ description: "Require existing comments before exports to use TSDoc block comment style."
45
+ },
46
+ hasSuggestions: false,
47
+ schema: [],
48
+ messages: {
49
+ [MESSAGE_ID]: "Comments immediately before exported declarations must use TSDoc /** ... */ style."
50
+ }
51
+ },
52
+ create(context) {
53
+ if (!isTypeScriptFilename(context.filename)) {
54
+ return {};
55
+ }
56
+ return {
57
+ ExportAllDeclaration(node) {
58
+ for (const comment of getLeadingCommentBlock(context.sourceCode, node)) {
59
+ if (isTSDocStyleComment(context.sourceCode, comment)) {
60
+ continue;
61
+ }
62
+ context.report({
63
+ messageId: MESSAGE_ID,
64
+ node
65
+ });
66
+ }
67
+ },
68
+ ExportDefaultDeclaration(node) {
69
+ for (const comment of getLeadingCommentBlock(context.sourceCode, node)) {
70
+ if (isTSDocStyleComment(context.sourceCode, comment)) {
71
+ continue;
72
+ }
73
+ context.report({
74
+ messageId: MESSAGE_ID,
75
+ node
76
+ });
77
+ }
78
+ },
79
+ ExportNamedDeclaration(node) {
80
+ for (const comment of getLeadingCommentBlock(context.sourceCode, node)) {
81
+ if (isTSDocStyleComment(context.sourceCode, comment)) {
82
+ continue;
83
+ }
84
+ context.report({
85
+ messageId: MESSAGE_ID,
86
+ node
87
+ });
88
+ }
89
+ }
90
+ };
91
+ }
92
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "eslint-plugin-tyrant",
3
+ "version": "0.1.0",
4
+ "description": "ESLint plugin with TypeScript-focused rules.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "format": "prettier --check .",
19
+ "build": "tsc -p tsconfig.json",
20
+ "lint": "eslint .",
21
+ "test": "node --import tsx --test tests/**/*.test.ts"
22
+ },
23
+ "keywords": [
24
+ "eslint",
25
+ "eslintplugin",
26
+ "typescript",
27
+ "tsdoc"
28
+ ],
29
+ "license": "MIT",
30
+ "peerDependencies": {
31
+ "eslint": "^9.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@eslint/js": "^9.35.0",
35
+ "@types/node": "^24.5.2",
36
+ "@typescript-eslint/parser": "^8.43.0",
37
+ "eslint": "^9.35.0",
38
+ "eslint-plugin-tsdoc": "^0.4.0",
39
+ "globals": "^16.4.0",
40
+ "jiti": "^2.6.1",
41
+ "prettier": "^3.6.2",
42
+ "prettier-plugin-package": "^2.0.0",
43
+ "tsx": "^4.20.5",
44
+ "typescript": "^5.9.2",
45
+ "typescript-eslint": "^8.43.0"
46
+ }
47
+ }