@api-extractor-tools/eslint-plugin 0.1.0-alpha.1 → 0.1.0-alpha.2

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/ARCHITECTURE.md CHANGED
@@ -97,11 +97,14 @@ Requires the TypeScript `override` keyword when the `@override` TSDoc tag is pre
97
97
 
98
98
  ### package-documentation
99
99
 
100
- Requires `@packageDocumentation` tag in files.
100
+ Enforces correct usage of the `@packageDocumentation` tag based on whether a file is a package entry point (barrel file). The rule automatically detects entry points by examining the nearest `package.json`.
101
+
102
+ - **Barrel files** (entry points): Requires `@packageDocumentation` tag to be present.
103
+ - **Non-barrel files**: Reports an error if `@packageDocumentation` tag is found.
101
104
 
102
105
  **Options:** None
103
106
 
104
- Note: This rule checks all files ESLint runs it on. To only check entry points, configure ESLint's `files` option or use the Node.js utilities to conditionally enable the rule.
107
+ Note: This rule uses `findPackageJson` and `isEntryPoint` from the entry-point utilities to determine barrel file status. If no `package.json` is found, the rule is skipped.
105
108
 
106
109
  ## Usage Examples
107
110
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @api-extractor-tools/eslint-plugin
2
2
 
3
+ ## 0.1.0-alpha.2
4
+
5
+ ### Minor Changes
6
+
7
+ - [#204](https://github.com/mike-north/api-extractor-tools/pull/204) [`3accc97`](https://github.com/mike-north/api-extractor-tools/commit/3accc97733d1b21eb7bbbe82b122a347e9b5ea76) Thanks [@mike-north](https://github.com/mike-north)! - fix: enforce @packageDocumentation only on package barrel files
8
+
9
+ The `package-documentation` rule now automatically detects whether a file is a package entry point by examining the nearest `package.json`. Barrel files are required to have the `@packageDocumentation` tag, and non-barrel files report an error if the tag is present. Previously the rule required the tag on every file regardless of whether it was an entry point.
10
+
3
11
  ## 0.1.0-alpha.1
4
12
 
5
13
  ### Patch Changes
@@ -6,5 +6,15 @@
6
6
  * All available ESLint rules.
7
7
  * @alpha
8
8
  */
9
- export declare const rules: { readonly 'extra-release-tag': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"extraReleaseTag", [import("./extra-release-tag").ExtraReleaseTagRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'forgotten-export': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"forgottenExport", [import("./forgotten-export").ForgottenExportRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'incompatible-release-tags': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"incompatibleReleaseTags", [import("./incompatible-release-tags").IncompatibleReleaseTagsRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'missing-release-tag': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingReleaseTag", [import("..").MissingReleaseTagRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'override-keyword': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingOverrideKeyword", [import("..").OverrideKeywordRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'package-documentation': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingPackageDocumentation", [import("..").PackageDocumentationRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'public-on-non-exported': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"publicOnNonExported", [import("./public-on-non-exported").PublicOnNonExportedRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'public-on-private-member': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"publicOnPrivateMember", [import("./public-on-private-member").PublicOnPrivateMemberRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>; readonly 'valid-enum-type': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingValue" | "invalidValue" | "multipleEnumTypes" | "invalidConstruct" | "missingEnumType", [import("..").ValidEnumTypeRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener> };
9
+ export declare const rules: {
10
+ readonly 'extra-release-tag': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"extraReleaseTag", [import("./extra-release-tag").ExtraReleaseTagRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
11
+ readonly 'forgotten-export': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"forgottenExport", [import("./forgotten-export").ForgottenExportRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
12
+ readonly 'incompatible-release-tags': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"incompatibleReleaseTags", [import("./incompatible-release-tags").IncompatibleReleaseTagsRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
13
+ readonly 'missing-release-tag': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingReleaseTag", [import("..").MissingReleaseTagRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
14
+ readonly 'override-keyword': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingOverrideKeyword", [import("..").OverrideKeywordRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
15
+ readonly 'package-documentation': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingPackageDocumentation" | "unexpectedPackageDocumentation", [import("..").PackageDocumentationRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
16
+ readonly 'public-on-non-exported': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"publicOnNonExported", [import("./public-on-non-exported").PublicOnNonExportedRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
17
+ readonly 'public-on-private-member': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"publicOnPrivateMember", [import("./public-on-private-member").PublicOnPrivateMemberRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
18
+ readonly 'valid-enum-type': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"missingValue" | "invalidValue" | "multipleEnumTypes" | "invalidConstruct" | "missingEnumType", [import("..").ValidEnumTypeRuleOptions], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
19
+ };
10
20
  //# sourceMappingURL=index.d.ts.map
@@ -1,8 +1,11 @@
1
1
  /**
2
- * ESLint rule requiring @packageDocumentation in files.
2
+ * ESLint rule requiring @packageDocumentation in package barrel files
3
+ * and disallowing it in non-barrel files.
3
4
  * @internal
4
5
  */
5
6
  import { ESLintUtils } from '@typescript-eslint/utils';
6
7
  import type { PackageDocumentationRuleOptions } from '../types';
7
- export declare const packageDocumentation: ESLintUtils.RuleModule<"missingPackageDocumentation", [PackageDocumentationRuleOptions], unknown, ESLintUtils.RuleListener>;
8
+ type MessageIds = 'missingPackageDocumentation' | 'unexpectedPackageDocumentation';
9
+ export declare const packageDocumentation: ESLintUtils.RuleModule<MessageIds, [PackageDocumentationRuleOptions], unknown, ESLintUtils.RuleListener>;
10
+ export {};
8
11
  //# sourceMappingURL=package-documentation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"package-documentation.d.ts","sourceRoot":"","sources":["../../src/rules/package-documentation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAKtD,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,UAAU,CAAA;AAS/D,eAAO,MAAM,oBAAoB,6HA4C/B,CAAA"}
1
+ {"version":3,"file":"package-documentation.d.ts","sourceRoot":"","sources":["../../src/rules/package-documentation.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAMtD,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,UAAU,CAAA;AAO/D,KAAK,UAAU,GACX,6BAA6B,GAC7B,gCAAgC,CAAA;AAEpC,eAAO,MAAM,oBAAoB,0GA+E/B,CAAA"}
@@ -1,22 +1,26 @@
1
1
  "use strict";
2
2
  /**
3
- * ESLint rule requiring @packageDocumentation in files.
3
+ * ESLint rule requiring @packageDocumentation in package barrel files
4
+ * and disallowing it in non-barrel files.
4
5
  * @internal
5
6
  */
6
7
  Object.defineProperty(exports, "__esModule", { value: true });
7
8
  exports.packageDocumentation = void 0;
9
+ const path = require("path");
8
10
  const utils_1 = require("@typescript-eslint/utils");
9
11
  const tsdoc_parser_1 = require("../utils/tsdoc-parser");
12
+ const entry_point_1 = require("../utils/entry-point");
10
13
  const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`);
11
14
  exports.packageDocumentation = createRule({
12
15
  name: 'package-documentation',
13
16
  meta: {
14
17
  type: 'suggestion',
15
18
  docs: {
16
- description: 'Require @packageDocumentation tag in files',
19
+ description: 'Require @packageDocumentation tag in package barrel files and disallow it in non-barrel files',
17
20
  },
18
21
  messages: {
19
22
  missingPackageDocumentation: 'File is missing a @packageDocumentation comment. Add a TSDoc comment with @packageDocumentation at the top of the file.',
23
+ unexpectedPackageDocumentation: '@packageDocumentation comment should only be in the package barrel file, not in this file.',
20
24
  },
21
25
  schema: [
22
26
  {
@@ -29,19 +33,46 @@ exports.packageDocumentation = createRule({
29
33
  defaultOptions: [{}],
30
34
  create(context) {
31
35
  const sourceCode = context.sourceCode;
36
+ const filename = context.filename;
32
37
  return {
33
38
  Program(node) {
39
+ // Find the nearest package.json to determine barrel file status
40
+ const fileDir = path.dirname(path.resolve(filename));
41
+ const pkgPath = (0, entry_point_1.findPackageJson)(fileDir);
42
+ // If no package.json is found, we can't determine barrel file status
43
+ if (!pkgPath) {
44
+ return;
45
+ }
46
+ const isBarrel = (0, entry_point_1.isEntryPoint)(filename, pkgPath);
34
47
  const tsdocComments = (0, tsdoc_parser_1.findAllTSDocComments)(sourceCode);
35
- for (const { parsed } of tsdocComments) {
36
- if (parsed.docComment && (0, tsdoc_parser_1.hasPackageDocumentation)(parsed.docComment)) {
37
- return;
48
+ if (isBarrel) {
49
+ // Barrel files must have @packageDocumentation
50
+ for (const { parsed } of tsdocComments) {
51
+ if (parsed.docComment &&
52
+ (0, tsdoc_parser_1.hasPackageDocumentation)(parsed.docComment)) {
53
+ return;
54
+ }
55
+ }
56
+ context.report({
57
+ node,
58
+ loc: { line: 1, column: 0 },
59
+ messageId: 'missingPackageDocumentation',
60
+ });
61
+ }
62
+ else {
63
+ // Non-barrel files must NOT have @packageDocumentation
64
+ for (const { comment, parsed } of tsdocComments) {
65
+ if (parsed.docComment &&
66
+ (0, tsdoc_parser_1.hasPackageDocumentation)(parsed.docComment)) {
67
+ context.report({
68
+ node,
69
+ loc: comment.loc,
70
+ messageId: 'unexpectedPackageDocumentation',
71
+ });
72
+ return;
73
+ }
38
74
  }
39
75
  }
40
- context.report({
41
- node,
42
- loc: { line: 1, column: 0 },
43
- messageId: 'missingPackageDocumentation',
44
- });
45
76
  },
46
77
  };
47
78
  },
@@ -1 +1 @@
1
- {"version":3,"file":"package-documentation.js","sourceRoot":"","sources":["../../src/rules/package-documentation.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,oDAAsD;AACtD,wDAG8B;AAG9B,MAAM,UAAU,GAAG,mBAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CACP,8FAA8F,IAAI,KAAK,CAC1G,CAAA;AAIY,QAAA,oBAAoB,GAAG,UAAU,CAG5C;IACA,IAAI,EAAE,uBAAuB;IAC7B,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EAAE,4CAA4C;SAC1D;QACD,QAAQ,EAAE;YACR,2BAA2B,EACzB,yHAAyH;SAC5H;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE;gBACd,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,cAAc,EAAE,CAAC,EAAE,CAAC;IACpB,MAAM,CAAC,OAAO;QACZ,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAA;QAErC,OAAO;YACL,OAAO,CAAC,IAAI;gBACV,MAAM,aAAa,GAAG,IAAA,mCAAoB,EAAC,UAAU,CAAC,CAAA;gBAEtD,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,aAAa,EAAE,CAAC;oBACvC,IAAI,MAAM,CAAC,UAAU,IAAI,IAAA,sCAAuB,EAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;wBACpE,OAAM;oBACR,CAAC;gBACH,CAAC;gBAED,OAAO,CAAC,MAAM,CAAC;oBACb,IAAI;oBACJ,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;oBAC3B,SAAS,EAAE,6BAA6B;iBACzC,CAAC,CAAA;YACJ,CAAC;SACF,CAAA;IACH,CAAC;CACF,CAAC,CAAA"}
1
+ {"version":3,"file":"package-documentation.js","sourceRoot":"","sources":["../../src/rules/package-documentation.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,6BAA4B;AAC5B,oDAAsD;AACtD,wDAG8B;AAC9B,sDAAoE;AAGpE,MAAM,UAAU,GAAG,mBAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CACP,8FAA8F,IAAI,KAAK,CAC1G,CAAA;AAMY,QAAA,oBAAoB,GAAG,UAAU,CAG5C;IACA,IAAI,EAAE,uBAAuB;IAC7B,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,+FAA+F;SAClG;QACD,QAAQ,EAAE;YACR,2BAA2B,EACzB,yHAAyH;YAC3H,8BAA8B,EAC5B,4FAA4F;SAC/F;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE,EAAE;gBACd,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,cAAc,EAAE,CAAC,EAAE,CAAC;IACpB,MAAM,CAAC,OAAO;QACZ,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAA;QACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;QAEjC,OAAO;YACL,OAAO,CAAC,IAAI;gBACV,gEAAgE;gBAChE,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;gBACpD,MAAM,OAAO,GAAG,IAAA,6BAAe,EAAC,OAAO,CAAC,CAAA;gBAExC,qEAAqE;gBACrE,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAM;gBACR,CAAC;gBAED,MAAM,QAAQ,GAAG,IAAA,0BAAY,EAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;gBAChD,MAAM,aAAa,GAAG,IAAA,mCAAoB,EAAC,UAAU,CAAC,CAAA;gBAEtD,IAAI,QAAQ,EAAE,CAAC;oBACb,+CAA+C;oBAC/C,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,aAAa,EAAE,CAAC;wBACvC,IACE,MAAM,CAAC,UAAU;4BACjB,IAAA,sCAAuB,EAAC,MAAM,CAAC,UAAU,CAAC,EAC1C,CAAC;4BACD,OAAM;wBACR,CAAC;oBACH,CAAC;oBAED,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;wBAC3B,SAAS,EAAE,6BAA6B;qBACzC,CAAC,CAAA;gBACJ,CAAC;qBAAM,CAAC;oBACN,uDAAuD;oBACvD,KAAK,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,aAAa,EAAE,CAAC;wBAChD,IACE,MAAM,CAAC,UAAU;4BACjB,IAAA,sCAAuB,EAAC,MAAM,CAAC,UAAU,CAAC,EAC1C,CAAC;4BACD,OAAO,CAAC,MAAM,CAAC;gCACb,IAAI;gCACJ,GAAG,EAAE,OAAO,CAAC,GAAG;gCAChB,SAAS,EAAE,gCAAgC;6BAC5C,CAAC,CAAA;4BACF,OAAM;wBACR,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAA;IACH,CAAC;CACF,CAAC,CAAA"}
package/dist/types.d.ts CHANGED
@@ -24,9 +24,23 @@ export interface MessageConfig {
24
24
  * @alpha
25
25
  */
26
26
  export interface ApiExtractorMessagesConfig {
27
- compilerMessageReporting?: { [messageId: string]: MessageConfig | undefined; default?: MessageConfig };
28
- extractorMessageReporting?: { 'ae-extra-release-tag'?: MessageConfig; 'ae-forgotten-export'?: MessageConfig; 'ae-incompatible-release-tags'?: MessageConfig; 'ae-internal-missing-underscore'?: MessageConfig; 'ae-missing-release-tag'?: MessageConfig; [messageId: string]: MessageConfig | undefined; default?: MessageConfig };
29
- tsdocMessageReporting?: { [messageId: string]: MessageConfig | undefined; default?: MessageConfig };
27
+ compilerMessageReporting?: {
28
+ [messageId: string]: MessageConfig | undefined;
29
+ default?: MessageConfig;
30
+ };
31
+ extractorMessageReporting?: {
32
+ 'ae-extra-release-tag'?: MessageConfig;
33
+ 'ae-forgotten-export'?: MessageConfig;
34
+ 'ae-incompatible-release-tags'?: MessageConfig;
35
+ 'ae-internal-missing-underscore'?: MessageConfig;
36
+ 'ae-missing-release-tag'?: MessageConfig;
37
+ [messageId: string]: MessageConfig | undefined;
38
+ default?: MessageConfig;
39
+ };
40
+ tsdocMessageReporting?: {
41
+ [messageId: string]: MessageConfig | undefined;
42
+ default?: MessageConfig;
43
+ };
30
44
  }
31
45
  /**
32
46
  * Partial representation of api-extractor.json relevant for this plugin.
@@ -80,9 +94,9 @@ export type OverrideKeywordRuleOptions = Record<string, never>;
80
94
  * Options for the package-documentation rule.
81
95
  *
82
96
  * @remarks
83
- * By default, checks all files. Node.js users can use the `/node` entry point
84
- * utilities to determine if a file is a package entry point and conditionally
85
- * enable this rule.
97
+ * Automatically detects whether a file is a package entry point by examining
98
+ * the nearest package.json. Requires `@packageDocumentation` on barrel files
99
+ * and reports an error if the tag is found on non-barrel files.
86
100
  *
87
101
  * @alpha
88
102
  */
@@ -74,9 +74,7 @@ export declare function extractEnumType(commentText: string): EnumTypeExtraction
74
74
  * @returns The comment text if a TSDoc comment exists, undefined otherwise
75
75
  * @alpha
76
76
  */
77
- export declare function getLeadingTSDocComment(sourceCode: {
78
- getCommentsBefore: (node: TSESTree.Node) => TSESTree.Comment[];
79
- }, node: TSESTree.Node): string | undefined;
77
+ export declare function getLeadingTSDocComment(sourceCode: { getCommentsBefore: (node: TSESTree.Node) => TSESTree.Comment[] }, node: TSESTree.Node): string | undefined;
80
78
  /**
81
79
  * Finds all TSDoc comments in a source file.
82
80
  *
@@ -84,9 +82,7 @@ export declare function getLeadingTSDocComment(sourceCode: {
84
82
  * @returns Array of comment objects with their parsed content
85
83
  * @alpha
86
84
  */
87
- export declare function findAllTSDocComments(sourceCode: {
88
- getAllComments: () => TSESTree.Comment[];
89
- }): Array<{
85
+ export declare function findAllTSDocComments(sourceCode: { getAllComments: () => TSESTree.Comment[] }): Array<{
90
86
  comment: TSESTree.Comment;
91
87
  parsed: ParserContext;
92
88
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-extractor-tools/eslint-plugin",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "ESLint plugin providing authoring-time feedback for API Extractor",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/eslint-plugin-public.d.ts",
@@ -41,7 +41,7 @@
41
41
  "tsd": "^0.33.0",
42
42
  "typescript": "5.8.3",
43
43
  "vitest": "^4.0.15",
44
- "@api-extractor-tools/declaration-file-normalizer": "0.0.1-alpha.2"
44
+ "@api-extractor-tools/declaration-file-normalizer": "0.1.0-alpha.6"
45
45
  },
46
46
  "scripts": {
47
47
  "clean": "rm -rf dist",
@@ -1,13 +1,16 @@
1
1
  /**
2
- * ESLint rule requiring @packageDocumentation in files.
2
+ * ESLint rule requiring @packageDocumentation in package barrel files
3
+ * and disallowing it in non-barrel files.
3
4
  * @internal
4
5
  */
5
6
 
7
+ import * as path from 'path'
6
8
  import { ESLintUtils } from '@typescript-eslint/utils'
7
9
  import {
8
10
  findAllTSDocComments,
9
11
  hasPackageDocumentation,
10
12
  } from '../utils/tsdoc-parser'
13
+ import { findPackageJson, isEntryPoint } from '../utils/entry-point'
11
14
  import type { PackageDocumentationRuleOptions } from '../types'
12
15
 
13
16
  const createRule = ESLintUtils.RuleCreator(
@@ -15,7 +18,9 @@ const createRule = ESLintUtils.RuleCreator(
15
18
  `https://github.com/mike-north/api-extractor-tools/blob/main/tools/eslint-plugin/docs/rules/${name}.md`,
16
19
  )
17
20
 
18
- type MessageIds = 'missingPackageDocumentation'
21
+ type MessageIds =
22
+ | 'missingPackageDocumentation'
23
+ | 'unexpectedPackageDocumentation'
19
24
 
20
25
  export const packageDocumentation = createRule<
21
26
  [PackageDocumentationRuleOptions],
@@ -25,11 +30,14 @@ export const packageDocumentation = createRule<
25
30
  meta: {
26
31
  type: 'suggestion',
27
32
  docs: {
28
- description: 'Require @packageDocumentation tag in files',
33
+ description:
34
+ 'Require @packageDocumentation tag in package barrel files and disallow it in non-barrel files',
29
35
  },
30
36
  messages: {
31
37
  missingPackageDocumentation:
32
38
  'File is missing a @packageDocumentation comment. Add a TSDoc comment with @packageDocumentation at the top of the file.',
39
+ unexpectedPackageDocumentation:
40
+ '@packageDocumentation comment should only be in the package barrel file, not in this file.',
33
41
  },
34
42
  schema: [
35
43
  {
@@ -42,22 +50,54 @@ export const packageDocumentation = createRule<
42
50
  defaultOptions: [{}],
43
51
  create(context) {
44
52
  const sourceCode = context.sourceCode
53
+ const filename = context.filename
45
54
 
46
55
  return {
47
56
  Program(node): void {
57
+ // Find the nearest package.json to determine barrel file status
58
+ const fileDir = path.dirname(path.resolve(filename))
59
+ const pkgPath = findPackageJson(fileDir)
60
+
61
+ // If no package.json is found, we can't determine barrel file status
62
+ if (!pkgPath) {
63
+ return
64
+ }
65
+
66
+ const isBarrel = isEntryPoint(filename, pkgPath)
48
67
  const tsdocComments = findAllTSDocComments(sourceCode)
49
68
 
50
- for (const { parsed } of tsdocComments) {
51
- if (parsed.docComment && hasPackageDocumentation(parsed.docComment)) {
52
- return
69
+ if (isBarrel) {
70
+ // Barrel files must have @packageDocumentation
71
+ for (const { parsed } of tsdocComments) {
72
+ if (
73
+ parsed.docComment &&
74
+ hasPackageDocumentation(parsed.docComment)
75
+ ) {
76
+ return
77
+ }
53
78
  }
54
- }
55
79
 
56
- context.report({
57
- node,
58
- loc: { line: 1, column: 0 },
59
- messageId: 'missingPackageDocumentation',
60
- })
80
+ context.report({
81
+ node,
82
+ loc: { line: 1, column: 0 },
83
+ messageId: 'missingPackageDocumentation',
84
+ })
85
+ } else {
86
+ // Non-barrel files must NOT have @packageDocumentation
87
+ for (const { comment, parsed } of tsdocComments) {
88
+ if (
89
+ parsed.docComment &&
90
+ hasPackageDocumentation(parsed.docComment)
91
+ ) {
92
+ context.report({
93
+ node,
94
+ loc: comment.loc,
95
+ messageId: 'unexpectedPackageDocumentation',
96
+ })
97
+ return
98
+ }
99
+ }
100
+ }
61
101
  },
62
102
  }
63
103
  },
package/src/types.ts CHANGED
@@ -108,9 +108,9 @@ export type OverrideKeywordRuleOptions = Record<string, never>
108
108
  * Options for the package-documentation rule.
109
109
  *
110
110
  * @remarks
111
- * By default, checks all files. Node.js users can use the `/node` entry point
112
- * utilities to determine if a file is a package entry point and conditionally
113
- * enable this rule.
111
+ * Automatically detects whether a file is a package entry point by examining
112
+ * the nearest package.json. Requires `@packageDocumentation` on barrel files
113
+ * and reports an error if the tag is found on non-barrel files.
114
114
  *
115
115
  * @alpha
116
116
  */
@@ -42,11 +42,25 @@ export type ApiExtractorLogLevel = 'error' | 'none' | 'warning';
42
42
  // @alpha
43
43
  export interface ApiExtractorMessagesConfig {
44
44
  // (undocumented)
45
- compilerMessageReporting?: { [messageId: string]: MessageConfig | undefined; default?: MessageConfig };
45
+ compilerMessageReporting?: {
46
+ [messageId: string]: MessageConfig | undefined;
47
+ default?: MessageConfig;
48
+ };
46
49
  // (undocumented)
47
- extractorMessageReporting?: { 'ae-extra-release-tag'?: MessageConfig; 'ae-forgotten-export'?: MessageConfig; 'ae-incompatible-release-tags'?: MessageConfig; 'ae-internal-missing-underscore'?: MessageConfig; 'ae-missing-release-tag'?: MessageConfig; [messageId: string]: MessageConfig | undefined; default?: MessageConfig };
50
+ extractorMessageReporting?: {
51
+ 'ae-extra-release-tag'?: MessageConfig;
52
+ 'ae-forgotten-export'?: MessageConfig;
53
+ 'ae-incompatible-release-tags'?: MessageConfig;
54
+ 'ae-internal-missing-underscore'?: MessageConfig;
55
+ 'ae-missing-release-tag'?: MessageConfig;
56
+ [messageId: string]: MessageConfig | undefined;
57
+ default?: MessageConfig;
58
+ };
48
59
  // (undocumented)
49
- tsdocMessageReporting?: { [messageId: string]: MessageConfig | undefined; default?: MessageConfig };
60
+ tsdocMessageReporting?: {
61
+ [messageId: string]: MessageConfig | undefined;
62
+ default?: MessageConfig;
63
+ };
50
64
  }
51
65
 
52
66
  // @alpha
@@ -73,9 +87,7 @@ export interface ExtraReleaseTagRuleOptions {
73
87
  }
74
88
 
75
89
  // @alpha
76
- export function findAllTSDocComments(sourceCode: {
77
- getAllComments: () => TSESTree.Comment[];
78
- }): Array<{
90
+ export function findAllTSDocComments(sourceCode: { getAllComments: () => TSESTree.Comment[] }): Array<{
79
91
  comment: TSESTree.Comment;
80
92
  parsed: ParserContext;
81
93
  }>;
@@ -86,9 +98,7 @@ export interface ForgottenExportRuleOptions {
86
98
  }
87
99
 
88
100
  // @alpha
89
- export function getLeadingTSDocComment(sourceCode: {
90
- getCommentsBefore: (node: TSESTree.Node) => TSESTree.Comment[];
91
- }, node: TSESTree.Node): string | undefined;
101
+ export function getLeadingTSDocComment(sourceCode: { getCommentsBefore: (node: TSESTree.Node) => TSESTree.Comment[] }, node: TSESTree.Node): string | undefined;
92
102
 
93
103
  // @alpha @override
94
104
  export function hasOverrideTag(docComment: DocComment): boolean;
@@ -157,7 +167,17 @@ export interface ResolvedEntryPoints {
157
167
  }
158
168
 
159
169
  // @alpha
160
- export const rules: { readonly 'extra-release-tag': RuleModule<"extraReleaseTag", [ExtraReleaseTagRuleOptions], unknown, RuleListener>; readonly 'forgotten-export': RuleModule<"forgottenExport", [ForgottenExportRuleOptions], unknown, RuleListener>; readonly 'incompatible-release-tags': RuleModule<"incompatibleReleaseTags", [IncompatibleReleaseTagsRuleOptions], unknown, RuleListener>; readonly 'missing-release-tag': RuleModule<"missingReleaseTag", [MissingReleaseTagRuleOptions], unknown, RuleListener>; readonly 'override-keyword': RuleModule<"missingOverrideKeyword", [OverrideKeywordRuleOptions], unknown, RuleListener>; readonly 'package-documentation': RuleModule<"missingPackageDocumentation", [PackageDocumentationRuleOptions], unknown, RuleListener>; readonly 'public-on-non-exported': RuleModule<"publicOnNonExported", [PublicOnNonExportedRuleOptions], unknown, RuleListener>; readonly 'public-on-private-member': RuleModule<"publicOnPrivateMember", [PublicOnPrivateMemberRuleOptions], unknown, RuleListener>; readonly 'valid-enum-type': RuleModule<"missingValue" | "invalidValue" | "multipleEnumTypes" | "invalidConstruct" | "missingEnumType", [ValidEnumTypeRuleOptions], unknown, RuleListener> };
170
+ export const rules: {
171
+ readonly 'extra-release-tag': RuleModule<"extraReleaseTag", [ExtraReleaseTagRuleOptions], unknown, RuleListener>;
172
+ readonly 'forgotten-export': RuleModule<"forgottenExport", [ForgottenExportRuleOptions], unknown, RuleListener>;
173
+ readonly 'incompatible-release-tags': RuleModule<"incompatibleReleaseTags", [IncompatibleReleaseTagsRuleOptions], unknown, RuleListener>;
174
+ readonly 'missing-release-tag': RuleModule<"missingReleaseTag", [MissingReleaseTagRuleOptions], unknown, RuleListener>;
175
+ readonly 'override-keyword': RuleModule<"missingOverrideKeyword", [OverrideKeywordRuleOptions], unknown, RuleListener>;
176
+ readonly 'package-documentation': RuleModule<"missingPackageDocumentation" | "unexpectedPackageDocumentation", [PackageDocumentationRuleOptions], unknown, RuleListener>;
177
+ readonly 'public-on-non-exported': RuleModule<"publicOnNonExported", [PublicOnNonExportedRuleOptions], unknown, RuleListener>;
178
+ readonly 'public-on-private-member': RuleModule<"publicOnPrivateMember", [PublicOnPrivateMemberRuleOptions], unknown, RuleListener>;
179
+ readonly 'valid-enum-type': RuleModule<"missingValue" | "invalidValue" | "multipleEnumTypes" | "invalidConstruct" | "missingEnumType", [ValidEnumTypeRuleOptions], unknown, RuleListener>;
180
+ };
161
181
 
162
182
  // @alpha
163
183
  export interface ValidEnumTypeRuleOptions {
@@ -1,7 +1,10 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
1
+ import { RuleTester } from '@typescript-eslint/rule-tester'
2
+ import { describe, it, expect, afterAll, afterEach, beforeEach } from 'vitest'
3
+ import * as tseslintParser from '@typescript-eslint/parser'
2
4
  import * as path from 'path'
3
5
  import * as fs from 'fs'
4
6
  import * as os from 'os'
7
+ import { packageDocumentation } from '../../src/rules/package-documentation.js'
5
8
  import {
6
9
  findPackageJson,
7
10
  isEntryPoint,
@@ -12,9 +15,49 @@ import {
12
15
  parseTSDocComment,
13
16
  } from '../../src/utils/tsdoc-parser.js'
14
17
 
18
+ // Wire up vitest for RuleTester
19
+ RuleTester.afterAll = afterAll
20
+ RuleTester.describe = describe
21
+ RuleTester.it = it
22
+
23
+ // Create a temp directory for RuleTester tests (must exist at module eval time)
24
+ const ruleTestDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-pd-rule-'))
25
+ const ruleTestSrcDir = path.join(ruleTestDir, 'src')
26
+ fs.mkdirSync(ruleTestSrcDir, { recursive: true })
27
+ fs.mkdirSync(path.join(ruleTestSrcDir, 'utils'), { recursive: true })
28
+
29
+ fs.writeFileSync(
30
+ path.join(ruleTestDir, 'package.json'),
31
+ JSON.stringify({ name: 'test-package', main: './src/index.ts' }, null, 2),
32
+ )
33
+
34
+ // Create placeholder files so paths resolve correctly
35
+ fs.writeFileSync(path.join(ruleTestSrcDir, 'index.ts'), '')
36
+ fs.writeFileSync(path.join(ruleTestSrcDir, 'helper.ts'), '')
37
+ fs.writeFileSync(path.join(ruleTestSrcDir, 'utils', 'deep.ts'), '')
38
+
39
+ const entryFile = path.join(ruleTestSrcDir, 'index.ts')
40
+ const helperFile = path.join(ruleTestSrcDir, 'helper.ts')
41
+ const deepFile = path.join(ruleTestSrcDir, 'utils', 'deep.ts')
42
+
43
+ const ruleTester = new RuleTester({
44
+ languageOptions: {
45
+ parser: tseslintParser,
46
+ parserOptions: {
47
+ ecmaVersion: 2022,
48
+ sourceType: 'module',
49
+ },
50
+ },
51
+ })
52
+
15
53
  describe('package-documentation', () => {
16
54
  let tempDir: string
17
55
 
56
+ afterAll(() => {
57
+ fs.rmSync(ruleTestDir, { recursive: true, force: true })
58
+ clearPackageJsonCache()
59
+ })
60
+
18
61
  beforeEach(() => {
19
62
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eslint-plugin-test-'))
20
63
  clearPackageJsonCache()
@@ -87,68 +130,72 @@ describe('package-documentation', () => {
87
130
  })
88
131
  })
89
132
 
90
- describe('rule logic simulation', () => {
91
- it('should pass when entry point has @packageDocumentation', () => {
92
- createPackageJson('./src/index.ts')
93
- const srcDir = createSourceDir()
94
- const indexPath = path.join(srcDir, 'index.ts')
95
-
96
- const code = `/**
97
- * This is the main entry point.
133
+ ruleTester.run('package-documentation', packageDocumentation, {
134
+ valid: [
135
+ // Entry point with @packageDocumentation — should pass
136
+ {
137
+ code: `/**
138
+ * Package entry point.
98
139
  * @packageDocumentation
99
140
  */
100
-
101
- export function foo() {}`
102
-
103
- fs.writeFileSync(indexPath, code)
104
-
105
- const pkgPath = findPackageJson(srcDir)
106
- expect(isEntryPoint(indexPath, pkgPath!)).toBe(true)
107
-
108
- // Simulate checking for @packageDocumentation
109
- const parsed = parseTSDocComment(`/**
110
- * This is the main entry point.
141
+ export function foo() {}`,
142
+ filename: entryFile,
143
+ },
144
+ // Non-entry-point without @packageDocumentation — should pass
145
+ {
146
+ code: `/**
147
+ * A helper function.
148
+ */
149
+ export function helper() {}`,
150
+ filename: helperFile,
151
+ },
152
+ // Non-entry-point with no comments at all — should pass
153
+ {
154
+ code: `export function helper() {}`,
155
+ filename: helperFile,
156
+ },
157
+ // Deep non-entry-point without @packageDocumentation — should pass
158
+ {
159
+ code: `export const x = 1`,
160
+ filename: deepFile,
161
+ },
162
+ ],
163
+ invalid: [
164
+ // Entry point without @packageDocumentation — should fail
165
+ {
166
+ code: `/**
167
+ * Missing package documentation.
168
+ */
169
+ export function foo() {}`,
170
+ filename: entryFile,
171
+ errors: [{ messageId: 'missingPackageDocumentation' as const }],
172
+ },
173
+ // Entry point with no comments at all — should fail
174
+ {
175
+ code: `export function foo() {}`,
176
+ filename: entryFile,
177
+ errors: [{ messageId: 'missingPackageDocumentation' as const }],
178
+ },
179
+ // Non-entry-point with @packageDocumentation — should fail
180
+ {
181
+ code: `/**
182
+ * This shouldn't be here.
111
183
  * @packageDocumentation
112
- */`)
113
- expect(hasPackageDocumentation(parsed.docComment)).toBe(true)
114
- })
115
-
116
- it('should fail when entry point lacks @packageDocumentation', () => {
117
- createPackageJson('./src/index.ts')
118
- const srcDir = createSourceDir()
119
- const indexPath = path.join(srcDir, 'index.ts')
120
-
121
- const code = `/**
122
- * This is the main entry point.
123
184
  */
124
-
125
- export function foo() {}`
126
-
127
- fs.writeFileSync(indexPath, code)
128
-
129
- const pkgPath = findPackageJson(srcDir)
130
- expect(isEntryPoint(indexPath, pkgPath!)).toBe(true)
131
-
132
- // Simulate checking for @packageDocumentation
133
- const parsed = parseTSDocComment(`/**
134
- * This is the main entry point.
135
- */`)
136
- expect(hasPackageDocumentation(parsed.docComment)).toBe(false)
137
- })
138
-
139
- it('should not require @packageDocumentation for non-entry points', () => {
140
- createPackageJson('./src/index.ts')
141
- const srcDir = createSourceDir()
142
-
143
- const indexPath = path.join(srcDir, 'index.ts')
144
- fs.writeFileSync(indexPath, '/** @packageDocumentation */ export {}')
145
-
146
- const helperPath = path.join(srcDir, 'helper.ts')
147
- fs.writeFileSync(helperPath, '// No package documentation needed')
148
-
149
- const pkgPath = findPackageJson(srcDir)
150
- // Helper is not an entry point, so no check needed
151
- expect(isEntryPoint(helperPath, pkgPath!)).toBe(false)
152
- })
185
+ export function helper() {}`,
186
+ filename: helperFile,
187
+ errors: [{ messageId: 'unexpectedPackageDocumentation' as const }],
188
+ },
189
+ // Deep non-entry-point with @packageDocumentation — should fail
190
+ {
191
+ code: `/**
192
+ * Wrong place for package docs.
193
+ * @packageDocumentation
194
+ */
195
+ export const x = 1`,
196
+ filename: deepFile,
197
+ errors: [{ messageId: 'unexpectedPackageDocumentation' as const }],
198
+ },
199
+ ],
153
200
  })
154
201
  })