@savvy-web/rslib-builder 0.3.0 → 0.4.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.
Files changed (4) hide show
  1. package/README.md +70 -18
  2. package/index.d.ts +567 -0
  3. package/index.js +445 -7
  4. package/package.json +14 -1
package/README.md CHANGED
@@ -4,9 +4,14 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D24.0.0-brightgreen)](https://nodejs.org)
6
6
 
7
- Build modern ESM Node.js libraries with zero configuration. Handles TypeScript
8
- declarations, package.json transformations, and PNPM workspace resolution
9
- automatically.
7
+ Build modern ESM Node.js libraries with minimal configuration. Handles
8
+ TypeScript declarations, package.json transformations, and PNPM workspace
9
+ resolution automatically.
10
+
11
+ Building TypeScript packages for npm involves repetitive setup: configuring
12
+ bundlers, generating declarations, transforming package.json exports, and
13
+ resolving workspace references. rslib-builder handles these tasks so you can
14
+ focus on your code.
10
15
 
11
16
  ## Features
12
17
 
@@ -19,8 +24,10 @@ automatically.
19
24
  outputs
20
25
  - **PNPM Integration** - Automatically resolves `catalog:` and `workspace:`
21
26
  references
22
- - **Package.json Transform** - Converts `.ts` exports and bin entries to `.js`,
23
- generates files array, removes dev-only fields
27
+ - **Package.json Transform** - Converts `.ts` exports to `.js`, generates files
28
+ array, removes dev-only fields
29
+ - **TSDoc Validation** - Pre-build TSDoc validation with automatic public API discovery
30
+ - **API Model Generation** - Optional API model output for documentation tooling
24
31
  - **Extensible** - Add custom RSlib/Rsbuild plugins for advanced use cases
25
32
 
26
33
  ## Prerequisites
@@ -43,6 +50,12 @@ Install the required peer dependencies:
43
50
  pnpm add -D @rslib/core @microsoft/api-extractor @typescript/native-preview
44
51
  ```
45
52
 
53
+ For TSDoc validation (optional):
54
+
55
+ ```bash
56
+ pnpm add -D eslint @typescript-eslint/parser eslint-plugin-tsdoc
57
+ ```
58
+
46
59
  ## Quick Start
47
60
 
48
61
  Extend the provided tsconfig for optimal settings:
@@ -96,16 +109,32 @@ rslib build --env-mode dev
96
109
  rslib build --env-mode npm
97
110
  ```
98
111
 
112
+ ## API Overview
113
+
114
+ The package exports a main builder and several plugins:
115
+
116
+ | Export | Description |
117
+ | ---------------------------- | --------------------------------------------- |
118
+ | `NodeLibraryBuilder` | Main API for building Node.js libraries |
119
+ | `AutoEntryPlugin` | Auto-extracts entry points from package.json |
120
+ | `DtsPlugin` | Generates TypeScript declarations with tsgo |
121
+ | `PackageJsonTransformPlugin` | Transforms package.json for distribution |
122
+ | `FilesArrayPlugin` | Generates files array for npm publishing |
123
+ | `TsDocLintPlugin` | Validates TSDoc comments before build |
124
+ | `TsDocConfigBuilder` | Utility for TSDoc configuration |
125
+ | `ImportGraph` | Traces TypeScript imports for file discovery |
126
+
99
127
  See [Configuration](./docs/guides/configuration.md) for all options.
100
128
 
101
129
  ## Plugins
102
130
 
103
131
  The builder includes several built-in plugins:
104
132
 
105
- 1. **AutoEntryPlugin** - Auto-extracts entry points from package.json exports
106
- 2. **PackageJsonTransformPlugin** - Transforms package.json for targets
133
+ 1. **TsDocLintPlugin** - Validates TSDoc comments before build (optional)
134
+ 2. **AutoEntryPlugin** - Auto-extracts entry points from package.json exports
107
135
  3. **DtsPlugin** - Generates TypeScript declarations with tsgo/API Extractor
108
- 4. **FilesArrayPlugin** - Generates files array, excludes source maps
136
+ 4. **PackageJsonTransformPlugin** - Transforms package.json for targets
137
+ 5. **FilesArrayPlugin** - Generates files array, excludes source maps
109
138
 
110
139
  ## How It Works
111
140
 
@@ -131,8 +160,31 @@ For detailed documentation, see the [docs/](./docs/) directory:
131
160
 
132
161
  ## Examples
133
162
 
134
- See the repository's own `rslib.config.ts` for a real-world example of the
135
- builder building itself.
163
+ This package builds itself using its own `NodeLibraryBuilder`. See
164
+ [`rslib.config.ts`](./rslib.config.ts) for a production example demonstrating:
165
+
166
+ - API model generation for documentation tooling
167
+ - External package configuration
168
+ - Custom package.json transformations
169
+ - Copy patterns for static files
170
+
171
+ ### Programmatic Usage
172
+
173
+ Use `ImportGraph` to discover all files reachable from your package exports:
174
+
175
+ ```typescript
176
+ import { ImportGraph } from '@savvy-web/rslib-builder';
177
+
178
+ const result = ImportGraph.fromPackageExports('./package.json', {
179
+ rootDir: process.cwd(),
180
+ });
181
+
182
+ console.log('Public API files:', result.files);
183
+ console.log('Entry points:', result.entries);
184
+ ```
185
+
186
+ See [Configuration](./docs/guides/configuration.md#importgraph-utility) for more
187
+ examples.
136
188
 
137
189
  ## Support
138
190
 
@@ -142,19 +194,19 @@ GitHub Issues, we cannot guarantee response times or resolution.
142
194
 
143
195
  For security vulnerabilities, please see [SECURITY.md](./SECURITY.md).
144
196
 
145
- ## License
197
+ ## Links
146
198
 
147
- [MIT](./LICENSE)
199
+ - [RSlib Documentation](https://rslib.dev/)
200
+ - [Rsbuild Plugin API](https://rsbuild.dev/plugins/dev/core)
201
+ - [API Extractor](https://api-extractor.com/)
202
+ - [PNPM Workspace](https://pnpm.io/workspaces)
203
+ - [PNPM Catalogs](https://pnpm.io/catalogs)
148
204
 
149
205
  ## Contributing
150
206
 
151
207
  Contributions welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup
152
208
  and guidelines.
153
209
 
154
- ## Links
210
+ ## License
155
211
 
156
- - [RSlib Documentation](https://rslib.dev/)
157
- - [Rsbuild Plugin API](https://rsbuild.dev/plugins/dev/core)
158
- - [API Extractor](https://api-extractor.com/)
159
- - [PNPM Workspace](https://pnpm.io/workspaces)
160
- - [PNPM Catalogs](https://pnpm.io/catalogs)
212
+ [MIT](./LICENSE)
package/index.d.ts CHANGED
@@ -46,6 +46,7 @@ import type { RawCopyPattern } from '@rspack/binding';
46
46
  import type { RsbuildPlugin } from '@rsbuild/core';
47
47
  import type { RslibConfig } from '@rslib/core';
48
48
  import type { SourceConfig } from '@rsbuild/core';
49
+ import ts from 'typescript';
49
50
 
50
51
  /**
51
52
  * Options for API model generation.
@@ -458,6 +459,374 @@ export declare interface FilesArrayPluginOptions<TTarget extends string = string
458
459
  target: TTarget;
459
460
  }
460
461
 
462
+ /**
463
+ * Analyzes TypeScript import relationships to discover all files
464
+ * reachable from specified entry points.
465
+ *
466
+ * @remarks
467
+ * This class uses the TypeScript compiler API to trace import statements
468
+ * and discover all files that are part of the public API. It handles:
469
+ *
470
+ * - Static imports: `import { foo } from "./module"`
471
+ * - Dynamic imports: `import("./module")`
472
+ * - Re-exports: `export * from "./module"` and `export { foo } from "./module"`
473
+ * - Circular imports (via visited set tracking)
474
+ *
475
+ * The class automatically filters out:
476
+ * - Files in node_modules
477
+ * - Declaration files (.d.ts)
478
+ * - Test files (*.test.ts, *.spec.ts)
479
+ * - Files in __test__ directories
480
+ *
481
+ * ## Static Methods vs Instance Methods
482
+ *
483
+ * For simple one-off analysis, use the static convenience methods:
484
+ * - {@link ImportGraph.fromEntries} - Trace from explicit entry paths
485
+ * - {@link ImportGraph.fromPackageExports} - Trace from package.json exports
486
+ *
487
+ * For repeated analysis or custom configuration, create an instance
488
+ * and use the instance methods which reuse the TypeScript program.
489
+ *
490
+ * @example
491
+ * Using static methods (recommended for most cases):
492
+ * ```typescript
493
+ * import { ImportGraph } from '@savvy-web/rslib-builder';
494
+ *
495
+ * // Trace from explicit entries
496
+ * const result = ImportGraph.fromEntries(
497
+ * ['./src/index.ts', './src/cli.ts'],
498
+ * { rootDir: process.cwd() }
499
+ * );
500
+ *
501
+ * // Trace from package.json exports
502
+ * const result = ImportGraph.fromPackageExports(
503
+ * './package.json',
504
+ * { rootDir: process.cwd() }
505
+ * );
506
+ * ```
507
+ *
508
+ * @example
509
+ * Using instance methods (for repeated analysis):
510
+ * ```typescript
511
+ * import { ImportGraph } from '@savvy-web/rslib-builder';
512
+ *
513
+ * const graph = new ImportGraph({ rootDir: '/path/to/project' });
514
+ *
515
+ * // Reuses the TypeScript program across multiple calls
516
+ * const libResult = graph.traceFromEntries(['./src/index.ts']);
517
+ * const cliResult = graph.traceFromEntries(['./src/cli.ts']);
518
+ * ```
519
+ *
520
+ * @public
521
+ */
522
+ export declare class ImportGraph {
523
+ private readonly options;
524
+ private readonly sys;
525
+ private program;
526
+ private compilerOptions;
527
+ private moduleResolutionCache;
528
+ constructor(options: ImportGraphOptions);
529
+ /**
530
+ * Trace all imports from the given entry points.
531
+ *
532
+ * @param entryPaths - Paths to entry files (relative to rootDir or absolute)
533
+ * @returns Deduplicated list of all reachable TypeScript files
534
+ */
535
+ traceFromEntries(entryPaths: string[]): ImportGraphResult;
536
+ /**
537
+ * Trace imports from package.json exports.
538
+ *
539
+ * @remarks
540
+ * Convenience method that extracts entry points from package.json
541
+ * using EntryExtractor, then traces all imports from those entries.
542
+ *
543
+ * @param packageJsonPath - Path to package.json (relative to rootDir or absolute)
544
+ * @returns Deduplicated list of all reachable TypeScript files
545
+ */
546
+ traceFromPackageExports(packageJsonPath: string): ImportGraphResult;
547
+ /**
548
+ * Initialize the TypeScript program for module resolution.
549
+ */
550
+ private initializeProgram;
551
+ /**
552
+ * Find tsconfig.json path.
553
+ */
554
+ private findTsConfig;
555
+ /**
556
+ * Resolve entry path to absolute path.
557
+ */
558
+ private resolveEntryPath;
559
+ /**
560
+ * Recursively trace imports from a source file.
561
+ */
562
+ private traceImports;
563
+ /**
564
+ * Extract all import/export module specifiers from a source file.
565
+ */
566
+ private extractImports;
567
+ /**
568
+ * Resolve a module specifier to an absolute file path.
569
+ */
570
+ private resolveImport;
571
+ /**
572
+ * Check if a path is an external module (node_modules).
573
+ */
574
+ private isExternalModule;
575
+ /**
576
+ * Check if a file should be included in results.
577
+ * Filters out test files and non-TypeScript files.
578
+ */
579
+ private isSourceFile;
580
+ /**
581
+ * Traces TypeScript imports from entry points.
582
+ *
583
+ * @remarks
584
+ * Static convenience method that creates an ImportGraph instance
585
+ * and traces imports in one call. For repeated analysis where you want
586
+ * to reuse the TypeScript program, create an instance and use
587
+ * {@link ImportGraph.traceFromEntries} instead.
588
+ *
589
+ * @param entryPaths - Paths to entry files (relative to rootDir or absolute)
590
+ * @param options - Import graph configuration options
591
+ * @returns All TypeScript files reachable from the entries
592
+ *
593
+ * @example
594
+ * ```typescript
595
+ * import { ImportGraph } from '@savvy-web/rslib-builder';
596
+ *
597
+ * const result = ImportGraph.fromEntries(
598
+ * ['./src/index.ts', './src/cli.ts'],
599
+ * { rootDir: process.cwd() }
600
+ * );
601
+ * console.log('Found files:', result.files);
602
+ * ```
603
+ */
604
+ static fromEntries(entryPaths: string[], options: ImportGraphOptions): ImportGraphResult;
605
+ /**
606
+ * Traces TypeScript imports from package.json exports.
607
+ *
608
+ * @remarks
609
+ * Static convenience method that extracts entry points from package.json exports
610
+ * and traces all imports to find public API files. For repeated analysis,
611
+ * create an instance and use {@link ImportGraph.traceFromPackageExports} instead.
612
+ *
613
+ * @param packageJsonPath - Path to package.json (relative to rootDir or absolute)
614
+ * @param options - Import graph configuration options
615
+ * @returns All TypeScript files reachable from the package exports
616
+ *
617
+ * @example
618
+ * ```typescript
619
+ * import { ImportGraph } from '@savvy-web/rslib-builder';
620
+ *
621
+ * const result = ImportGraph.fromPackageExports(
622
+ * './package.json',
623
+ * { rootDir: process.cwd() }
624
+ * );
625
+ * console.log('Public API files:', result.files);
626
+ * ```
627
+ */
628
+ static fromPackageExports(packageJsonPath: string, options: ImportGraphOptions): ImportGraphResult;
629
+ }
630
+
631
+ /**
632
+ * Structured error from import graph analysis.
633
+ *
634
+ * @remarks
635
+ * Provides detailed error information including the error type for
636
+ * programmatic handling, a human-readable message, and the relevant
637
+ * file path when applicable.
638
+ *
639
+ * @example
640
+ * ```typescript
641
+ * import type { ImportGraphError } from '@savvy-web/rslib-builder';
642
+ *
643
+ * function handleErrors(errors: ImportGraphError[]): void {
644
+ * for (const error of errors) {
645
+ * switch (error.type) {
646
+ * case 'tsconfig_not_found':
647
+ * console.warn('No tsconfig.json found, using defaults');
648
+ * break;
649
+ * case 'entry_not_found':
650
+ * console.error(`Missing entry: ${error.path}`);
651
+ * break;
652
+ * default:
653
+ * console.error(error.message);
654
+ * }
655
+ * }
656
+ * }
657
+ * ```
658
+ *
659
+ * @public
660
+ */
661
+ export declare interface ImportGraphError {
662
+ /**
663
+ * The type of error that occurred.
664
+ *
665
+ * @remarks
666
+ * Use this field for programmatic error handling to distinguish
667
+ * between different failure modes.
668
+ */
669
+ type: ImportGraphErrorType;
670
+ /**
671
+ * Human-readable error message.
672
+ *
673
+ * @remarks
674
+ * Suitable for logging or displaying to users.
675
+ */
676
+ message: string;
677
+ /**
678
+ * The file path related to the error, if applicable.
679
+ *
680
+ * @remarks
681
+ * Present for errors related to specific files like missing entries
682
+ * or file read failures.
683
+ */
684
+ path?: string;
685
+ }
686
+
687
+ /**
688
+ * Types of errors that can occur during import graph analysis.
689
+ *
690
+ * @remarks
691
+ * These error types allow consumers to handle different failure modes
692
+ * appropriately. For example, a missing tsconfig might be handled differently
693
+ * than a missing entry file.
694
+ *
695
+ * @public
696
+ */
697
+ export declare type ImportGraphErrorType = "tsconfig_not_found" | "tsconfig_read_error" | "tsconfig_parse_error" | "package_json_not_found" | "package_json_parse_error" | "entry_not_found" | "file_read_error";
698
+
699
+ /**
700
+ * Options for configuring the ImportGraph analyzer.
701
+ *
702
+ * @remarks
703
+ * These options control how the ImportGraph traverses and resolves
704
+ * TypeScript module imports. The `rootDir` is required and serves as
705
+ * the base for resolving relative paths and finding the tsconfig.json.
706
+ *
707
+ * @example
708
+ * ```typescript
709
+ * import type { ImportGraphOptions } from '@savvy-web/rslib-builder';
710
+ *
711
+ * const options: ImportGraphOptions = {
712
+ * rootDir: '/path/to/project',
713
+ * tsconfigPath: './tsconfig.build.json',
714
+ * };
715
+ * ```
716
+ *
717
+ * @public
718
+ */
719
+ export declare interface ImportGraphOptions {
720
+ /**
721
+ * The project root directory.
722
+ *
723
+ * @remarks
724
+ * All relative paths (entry points, tsconfig path) are resolved from this directory.
725
+ * This should typically be the package root containing your `package.json`.
726
+ */
727
+ rootDir: string;
728
+ /**
729
+ * Custom path to the TypeScript configuration file.
730
+ *
731
+ * @remarks
732
+ * If not provided, the analyzer searches for `tsconfig.json` starting from `rootDir`
733
+ * and walking up the directory tree. The tsconfig is used for module resolution
734
+ * settings including path aliases and module resolution strategy.
735
+ *
736
+ * @defaultValue Searches for tsconfig.json from rootDir
737
+ */
738
+ tsconfigPath?: string;
739
+ /**
740
+ * Custom TypeScript system for file operations.
741
+ *
742
+ * @remarks
743
+ * This is primarily used for testing to provide a mock filesystem.
744
+ * In production use, this defaults to `ts.sys` which uses the real filesystem.
745
+ *
746
+ * @defaultValue ts.sys
747
+ * @internal
748
+ */
749
+ sys?: ts.System;
750
+ /**
751
+ * Additional patterns to exclude from results.
752
+ *
753
+ * @remarks
754
+ * Patterns are matched against file paths using simple string inclusion.
755
+ * Use this to exclude files that don't match the default test file patterns.
756
+ *
757
+ * The default exclusions are always applied:
758
+ * - `.test.` and `.spec.` files
759
+ * - `__test__` and `__tests__` directories
760
+ * - `.d.ts` declaration files
761
+ *
762
+ * @example
763
+ * ```typescript
764
+ * const graph = new ImportGraph({
765
+ * rootDir: process.cwd(),
766
+ * excludePatterns: ['/fixtures/', '/mocks/', '.stories.'],
767
+ * });
768
+ * ```
769
+ *
770
+ * @defaultValue []
771
+ */
772
+ excludePatterns?: string[];
773
+ }
774
+
775
+ /**
776
+ * Result of import graph analysis.
777
+ *
778
+ * @remarks
779
+ * Contains the complete set of TypeScript source files discovered by tracing
780
+ * imports from entry points. The analysis is non-fatal: errors are collected
781
+ * and tracing continues for other paths even when some imports fail to resolve.
782
+ *
783
+ * @example
784
+ * ```typescript
785
+ * import type { ImportGraphResult } from '@savvy-web/rslib-builder';
786
+ *
787
+ * function processResult(result: ImportGraphResult): void {
788
+ * if (result.errors.length > 0) {
789
+ * console.warn('Some imports could not be resolved:', result.errors);
790
+ * }
791
+ * console.log(`Found ${result.files.length} files from ${result.entries.length} entries`);
792
+ * }
793
+ * ```
794
+ *
795
+ * @public
796
+ */
797
+ export declare interface ImportGraphResult {
798
+ /**
799
+ * All TypeScript source files reachable from the entry points.
800
+ *
801
+ * @remarks
802
+ * Paths are absolute, normalized, and sorted alphabetically.
803
+ * Test files (`.test.ts`, `.spec.ts`) and test directories (`__test__`, `__tests__`)
804
+ * are automatically filtered out from results.
805
+ */
806
+ files: string[];
807
+ /**
808
+ * The entry points that were traced.
809
+ *
810
+ * @remarks
811
+ * Paths are absolute and normalized. These are the starting points
812
+ * from which the import graph was traversed.
813
+ */
814
+ entries: string[];
815
+ /**
816
+ * Errors encountered during import graph analysis.
817
+ *
818
+ * @remarks
819
+ * These errors are non-fatal: tracing continues despite individual failures.
820
+ * Common errors include missing entry files, unresolvable imports,
821
+ * or tsconfig parsing failures.
822
+ *
823
+ * Each error includes a `type` field for programmatic handling and
824
+ * a human-readable `message`. Some errors also include a `path` field
825
+ * indicating the relevant file.
826
+ */
827
+ errors: ImportGraphError[];
828
+ }
829
+
461
830
  /**
462
831
  * Builder for Node.js ESM libraries using RSlib.
463
832
  *
@@ -696,6 +1065,42 @@ export declare interface NodeLibraryBuilderOptions {
696
1065
  * ```
697
1066
  */
698
1067
  apiModel?: ApiModelOptions | boolean;
1068
+ /**
1069
+ * Options for TSDoc lint validation.
1070
+ * When enabled, validates TSDoc comments before the build starts.
1071
+ *
1072
+ * @remarks
1073
+ * Uses ESLint with `eslint-plugin-tsdoc` to validate TSDoc syntax.
1074
+ * By default, throws errors in CI environments and logs errors locally.
1075
+ * The generated `tsdoc.json` config is persisted locally for IDE integration.
1076
+ *
1077
+ * @example
1078
+ * Enable with defaults (throws in CI, errors locally):
1079
+ * ```typescript
1080
+ * import { NodeLibraryBuilder } from '@savvy-web/rslib-builder';
1081
+ *
1082
+ * export default NodeLibraryBuilder.create({
1083
+ * tsdocLint: true,
1084
+ * });
1085
+ * ```
1086
+ *
1087
+ * @example
1088
+ * Enable with custom configuration:
1089
+ * ```typescript
1090
+ * import { NodeLibraryBuilder } from '@savvy-web/rslib-builder';
1091
+ *
1092
+ * export default NodeLibraryBuilder.create({
1093
+ * tsdocLint: {
1094
+ * tsdoc: {
1095
+ * tagDefinitions: [{ tagName: '@error', syntaxKind: 'block' }],
1096
+ * },
1097
+ * onError: 'throw',
1098
+ * persistConfig: true,
1099
+ * },
1100
+ * });
1101
+ * ```
1102
+ */
1103
+ tsdocLint?: TsDocLintPluginOptions | boolean;
699
1104
  }
700
1105
 
701
1106
  /**
@@ -1001,6 +1406,168 @@ export declare class TsDocConfigBuilder {
1001
1406
  private static syntaxKindToString;
1002
1407
  }
1003
1408
 
1409
+ /**
1410
+ * Error behavior for TSDoc lint errors.
1411
+ *
1412
+ * @remarks
1413
+ * - `"warn"`: Log warnings but continue the build
1414
+ * - `"error"`: Log errors but continue the build
1415
+ * - `"throw"`: Fail the build with an error
1416
+ *
1417
+ * @public
1418
+ */
1419
+ export declare type TsDocLintErrorBehavior = "warn" | "error" | "throw";
1420
+
1421
+ /**
1422
+ * Creates a plugin to validate TSDoc comments before build using ESLint with eslint-plugin-tsdoc.
1423
+ *
1424
+ * @remarks
1425
+ * This plugin runs TSDoc validation during the `onBeforeBuild` hook, ensuring that
1426
+ * documentation errors are caught before compilation begins. It generates a virtual
1427
+ * `tsdoc.json` configuration file that can be persisted for IDE and tool integration.
1428
+ *
1429
+ * ## Features
1430
+ *
1431
+ * - Programmatic ESLint execution with `eslint-plugin-tsdoc`
1432
+ * - Configurable error handling (warn, error, throw)
1433
+ * - Automatic CI detection for stricter defaults
1434
+ * - Optional tsdoc.json persistence for tool integration
1435
+ * - Automatic file discovery via import graph analysis
1436
+ * - Customizable file patterns when needed
1437
+ *
1438
+ * ## Error Handling
1439
+ *
1440
+ * | Environment | Default Behavior | On Lint Errors |
1441
+ * |-------------|------------------|----------------|
1442
+ * | Local | `"error"` | Log and continue |
1443
+ * | CI | `"throw"` | Fail the build |
1444
+ *
1445
+ * ## Required Dependencies
1446
+ *
1447
+ * This plugin requires the following optional peer dependencies:
1448
+ * - `eslint`
1449
+ * - `@typescript-eslint/parser`
1450
+ * - `eslint-plugin-tsdoc`
1451
+ *
1452
+ * Install with: `pnpm add -D eslint @typescript-eslint/parser eslint-plugin-tsdoc`
1453
+ *
1454
+ * @param options - Plugin configuration options
1455
+ * @returns An Rsbuild plugin that validates TSDoc comments before the build
1456
+ *
1457
+ * @example
1458
+ * ```typescript
1459
+ * import { TsDocLintPlugin } from '@savvy-web/rslib-builder';
1460
+ *
1461
+ * export default defineConfig({
1462
+ * plugins: [
1463
+ * TsDocLintPlugin({
1464
+ * onError: 'throw',
1465
+ * persistConfig: true,
1466
+ * }),
1467
+ * ],
1468
+ * });
1469
+ * ```
1470
+ *
1471
+ * @public
1472
+ */
1473
+ export declare const TsDocLintPlugin: (options?: TsDocLintPluginOptions) => RsbuildPlugin;
1474
+
1475
+ /**
1476
+ * Options for the TSDoc lint plugin.
1477
+ *
1478
+ * @remarks
1479
+ * This plugin validates TSDoc comments in your source files before the build
1480
+ * starts using ESLint with `eslint-plugin-tsdoc`. It helps catch documentation
1481
+ * errors early in the development cycle.
1482
+ *
1483
+ * @example
1484
+ * Enable with defaults (throws in CI, errors locally):
1485
+ * ```typescript
1486
+ * import { TsDocLintPlugin } from '@savvy-web/rslib-builder';
1487
+ *
1488
+ * export default defineConfig({
1489
+ * plugins: [TsDocLintPlugin()],
1490
+ * });
1491
+ * ```
1492
+ *
1493
+ * @example
1494
+ * Custom configuration:
1495
+ * ```typescript
1496
+ * import { TsDocLintPlugin } from '@savvy-web/rslib-builder';
1497
+ *
1498
+ * export default defineConfig({
1499
+ * plugins: [
1500
+ * TsDocLintPlugin({
1501
+ * tsdoc: {
1502
+ * tagDefinitions: [{ tagName: '@error', syntaxKind: 'block' }],
1503
+ * },
1504
+ * onError: 'throw',
1505
+ * persistConfig: true,
1506
+ * }),
1507
+ * ],
1508
+ * });
1509
+ * ```
1510
+ *
1511
+ * @public
1512
+ */
1513
+ export declare interface TsDocLintPluginOptions {
1514
+ /**
1515
+ * Whether to enable TSDoc linting.
1516
+ * @defaultValue true
1517
+ */
1518
+ enabled?: boolean;
1519
+ /**
1520
+ * TSDoc configuration for custom tag definitions.
1521
+ * Uses the same options as the DtsPlugin's apiModel.tsdoc option.
1522
+ *
1523
+ * @remarks
1524
+ * By default, all standard tag groups (core, extended, discretionary) are
1525
+ * enabled. Custom tags defined in `tagDefinitions` are automatically
1526
+ * supported.
1527
+ */
1528
+ tsdoc?: TsDocOptions;
1529
+ /**
1530
+ * Override automatic file discovery with explicit file paths or glob patterns.
1531
+ *
1532
+ * @remarks
1533
+ * By default, TsDocLintPlugin uses import graph analysis to discover files
1534
+ * from your package's exports. This ensures only public API files are linted.
1535
+ *
1536
+ * Use this option only when you need to lint specific files that aren't
1537
+ * part of the export graph, or to override the automatic discovery.
1538
+ *
1539
+ * When specified as glob patterns, test files and `__test__` directories
1540
+ * are still excluded unless explicitly included.
1541
+ *
1542
+ * @example
1543
+ * ```typescript
1544
+ * // Explicit patterns override automatic discovery
1545
+ * TsDocLintPlugin({
1546
+ * include: ["src/**\/*.ts", "!**\/*.test.ts"],
1547
+ * })
1548
+ * ```
1549
+ */
1550
+ include?: string[];
1551
+ /**
1552
+ * How to handle TSDoc lint errors.
1553
+ * - `"warn"`: Log warnings but continue the build
1554
+ * - `"error"`: Log errors but continue the build
1555
+ * - `"throw"`: Fail the build with an error
1556
+ *
1557
+ * @defaultValue `"throw"` in CI environments, `"error"` locally
1558
+ */
1559
+ onError?: TsDocLintErrorBehavior;
1560
+ /**
1561
+ * Persist tsdoc.json to disk for tool integration (ESLint, IDEs).
1562
+ * - `true`: Write to project root as "tsdoc.json"
1563
+ * - `PathLike`: Write to specified path
1564
+ * - `false`: Clean up after linting
1565
+ *
1566
+ * @defaultValue `true` when not in CI, `false` in CI environments
1567
+ */
1568
+ persistConfig?: boolean | PathLike;
1569
+ }
1570
+
1004
1571
  /**
1005
1572
  * Options for tsdoc-metadata.json generation.
1006
1573
  * @public
package/index.js CHANGED
@@ -3,13 +3,14 @@ import * as __rspack_external_node_path_c5b9b54f from "node:path";
3
3
  import { __webpack_require__ } from "./rslib-runtime.js";
4
4
  import { constants, existsSync, writeFileSync } from "node:fs";
5
5
  import { defineConfig } from "@rslib/core";
6
- import { access, copyFile, mkdir, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
6
+ import { access, copyFile, mkdir, readFile, readdir, rm, stat, unlink as promises_unlink, writeFile } from "node:fs/promises";
7
7
  import { logger as core_logger } from "@rsbuild/core";
8
8
  import picocolors from "picocolors";
9
9
  import { getWorkspaceRoot } from "workspace-tools";
10
10
  import { spawn } from "node:child_process";
11
11
  import { StandardTags, Standardization, TSDocTagSyntaxKind } from "@microsoft/tsdoc";
12
- import { createCompilerHost, findConfigFile, formatDiagnostic, parseJsonConfigFileContent, readConfigFile, sys } from "typescript";
12
+ import deep_equal from "deep-equal";
13
+ import typescript, { createCompilerHost, findConfigFile, formatDiagnostic, parseJsonConfigFileContent, readConfigFile, sys } from "typescript";
13
14
  import { createRequire } from "node:module";
14
15
  import { inspect } from "node:util";
15
16
  import sort_package_json from "sort-package-json";
@@ -446,7 +447,14 @@ class TsDocConfigBuilder {
446
447
  if (tagDefinitions.length > 0) tsdocConfig.tagDefinitions = tagDefinitions;
447
448
  if (Object.keys(supportForTags).length > 0) tsdocConfig.supportForTags = supportForTags;
448
449
  const configPath = (0, external_node_path_.join)(outputDir, "tsdoc.json");
449
- await writeFile(configPath, JSON.stringify(tsdocConfig, null, 2));
450
+ if (existsSync(configPath)) try {
451
+ const existingContent = await readFile(configPath, "utf-8");
452
+ const existingConfig = JSON.parse(existingContent);
453
+ if (deep_equal(existingConfig, tsdocConfig, {
454
+ strict: true
455
+ })) return configPath;
456
+ } catch {}
457
+ await writeFile(configPath, `${JSON.stringify(tsdocConfig, null, "\t")}\n`);
450
458
  return configPath;
451
459
  }
452
460
  static syntaxKindToString(kind) {
@@ -630,7 +638,7 @@ async function bundleDtsFiles(options) {
630
638
  let persistedTsdocConfigPath;
631
639
  if (tsdocConfigPath) if (shouldPersist) persistedTsdocConfigPath = tsdocConfigPath;
632
640
  else try {
633
- await unlink(tsdocConfigPath);
641
+ await promises_unlink(tsdocConfigPath);
634
642
  } catch {}
635
643
  return {
636
644
  bundledFiles,
@@ -792,7 +800,7 @@ function runTsgo(options) {
792
800
  });
793
801
  await copyFile(file.path, newPath);
794
802
  log.global.info(`Renamed ${file.relativePath} -> ${originalDtsPath} (from temp api-extractor)`);
795
- await unlink(file.path);
803
+ await promises_unlink(file.path);
796
804
  }
797
805
  }
798
806
  allDtsFiles.length = 0;
@@ -1440,6 +1448,430 @@ const PackageJsonTransformPlugin = (options = {})=>{
1440
1448
  }
1441
1449
  };
1442
1450
  };
1451
+ class ImportGraph {
1452
+ options;
1453
+ sys;
1454
+ program = null;
1455
+ compilerOptions = null;
1456
+ moduleResolutionCache = null;
1457
+ constructor(options){
1458
+ this.options = options;
1459
+ this.sys = options.sys ?? typescript.sys;
1460
+ }
1461
+ traceFromEntries(entryPaths) {
1462
+ const errors = [];
1463
+ const visited = new Set();
1464
+ const entries = [];
1465
+ const initResult = this.initializeProgram();
1466
+ if (!initResult.success) return {
1467
+ files: [],
1468
+ entries: [],
1469
+ errors: [
1470
+ initResult.error
1471
+ ]
1472
+ };
1473
+ for (const entryPath of entryPaths){
1474
+ const absolutePath = this.resolveEntryPath(entryPath);
1475
+ if (!this.sys.fileExists(absolutePath)) {
1476
+ errors.push({
1477
+ type: "entry_not_found",
1478
+ message: `Entry file not found: ${entryPath}`,
1479
+ path: absolutePath
1480
+ });
1481
+ continue;
1482
+ }
1483
+ entries.push(absolutePath);
1484
+ this.traceImports(absolutePath, visited, errors);
1485
+ }
1486
+ const files = Array.from(visited).filter((file)=>this.isSourceFile(file));
1487
+ return {
1488
+ files: files.sort(),
1489
+ entries,
1490
+ errors
1491
+ };
1492
+ }
1493
+ traceFromPackageExports(packageJsonPath) {
1494
+ const absolutePath = this.resolveEntryPath(packageJsonPath);
1495
+ let packageJson;
1496
+ try {
1497
+ const content = this.sys.readFile(absolutePath);
1498
+ if (!content) return {
1499
+ files: [],
1500
+ entries: [],
1501
+ errors: [
1502
+ {
1503
+ type: "package_json_not_found",
1504
+ message: `Failed to read package.json: File not found at ${absolutePath}`,
1505
+ path: absolutePath
1506
+ }
1507
+ ]
1508
+ };
1509
+ packageJson = JSON.parse(content);
1510
+ } catch (error) {
1511
+ const message = error instanceof Error ? error.message : String(error);
1512
+ return {
1513
+ files: [],
1514
+ entries: [],
1515
+ errors: [
1516
+ {
1517
+ type: "package_json_parse_error",
1518
+ message: `Failed to parse package.json: ${message}`,
1519
+ path: absolutePath
1520
+ }
1521
+ ]
1522
+ };
1523
+ }
1524
+ const extractor = new EntryExtractor();
1525
+ const { entries } = extractor.extract(packageJson);
1526
+ const packageDir = (0, external_node_path_.dirname)(absolutePath);
1527
+ const entryPaths = Object.values(entries).map((p)=>(0, external_node_path_.resolve)(packageDir, p));
1528
+ return this.traceFromEntries(entryPaths);
1529
+ }
1530
+ initializeProgram() {
1531
+ if (this.program) return {
1532
+ success: true
1533
+ };
1534
+ const configPath = this.findTsConfig();
1535
+ if (!configPath) return {
1536
+ success: false,
1537
+ error: {
1538
+ type: "tsconfig_not_found",
1539
+ message: `No tsconfig.json found in ${this.options.rootDir}`,
1540
+ path: this.options.rootDir
1541
+ }
1542
+ };
1543
+ const configFile = typescript.readConfigFile(configPath, (path)=>this.sys.readFile(path));
1544
+ if (configFile.error) {
1545
+ const message = typescript.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
1546
+ return {
1547
+ success: false,
1548
+ error: {
1549
+ type: "tsconfig_read_error",
1550
+ message: `Failed to read tsconfig.json: ${message}`,
1551
+ path: configPath
1552
+ }
1553
+ };
1554
+ }
1555
+ const parsed = typescript.parseJsonConfigFileContent(configFile.config, this.sys, (0, external_node_path_.dirname)(configPath));
1556
+ if (parsed.errors.length > 0) {
1557
+ const messages = parsed.errors.map((e)=>typescript.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
1558
+ return {
1559
+ success: false,
1560
+ error: {
1561
+ type: "tsconfig_parse_error",
1562
+ message: `Failed to parse tsconfig.json: ${messages}`,
1563
+ path: configPath
1564
+ }
1565
+ };
1566
+ }
1567
+ this.compilerOptions = parsed.options;
1568
+ this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
1569
+ const host = typescript.createCompilerHost(this.compilerOptions, true);
1570
+ host.getCurrentDirectory = ()=>this.options.rootDir;
1571
+ this.program = typescript.createProgram([], this.compilerOptions, host);
1572
+ return {
1573
+ success: true
1574
+ };
1575
+ }
1576
+ findTsConfig() {
1577
+ if (this.options.tsconfigPath) {
1578
+ const customPath = (0, external_node_path_.isAbsolute)(this.options.tsconfigPath) ? this.options.tsconfigPath : (0, external_node_path_.resolve)(this.options.rootDir, this.options.tsconfigPath);
1579
+ if (this.sys.fileExists(customPath)) return customPath;
1580
+ return null;
1581
+ }
1582
+ const configPath = typescript.findConfigFile(this.options.rootDir, (path)=>this.sys.fileExists(path));
1583
+ return configPath ?? null;
1584
+ }
1585
+ resolveEntryPath(entryPath) {
1586
+ if ((0, external_node_path_.isAbsolute)(entryPath)) return (0, external_node_path_.normalize)(entryPath);
1587
+ return (0, external_node_path_.normalize)((0, external_node_path_.resolve)(this.options.rootDir, entryPath));
1588
+ }
1589
+ traceImports(filePath, visited, errors) {
1590
+ const normalizedPath = (0, external_node_path_.normalize)(filePath);
1591
+ if (visited.has(normalizedPath)) return;
1592
+ if (this.isExternalModule(normalizedPath)) return;
1593
+ visited.add(normalizedPath);
1594
+ const content = this.sys.readFile(normalizedPath);
1595
+ if (!content) return void errors.push({
1596
+ type: "file_read_error",
1597
+ message: `Failed to read file: ${normalizedPath}`,
1598
+ path: normalizedPath
1599
+ });
1600
+ const sourceFile = typescript.createSourceFile(normalizedPath, content, typescript.ScriptTarget.Latest, true);
1601
+ const imports = this.extractImports(sourceFile);
1602
+ for (const importPath of imports){
1603
+ const resolved = this.resolveImport(importPath, normalizedPath);
1604
+ if (resolved) this.traceImports(resolved, visited, errors);
1605
+ }
1606
+ }
1607
+ extractImports(sourceFile) {
1608
+ const imports = [];
1609
+ const visit = (node)=>{
1610
+ if (typescript.isImportDeclaration(node)) {
1611
+ const specifier = node.moduleSpecifier;
1612
+ if (typescript.isStringLiteral(specifier)) imports.push(specifier.text);
1613
+ } else if (typescript.isExportDeclaration(node)) {
1614
+ const specifier = node.moduleSpecifier;
1615
+ if (specifier && typescript.isStringLiteral(specifier)) imports.push(specifier.text);
1616
+ } else if (typescript.isCallExpression(node)) {
1617
+ const expression = node.expression;
1618
+ if (expression.kind === typescript.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
1619
+ const arg = node.arguments[0];
1620
+ if (arg && typescript.isStringLiteral(arg)) imports.push(arg.text);
1621
+ }
1622
+ }
1623
+ typescript.forEachChild(node, visit);
1624
+ };
1625
+ visit(sourceFile);
1626
+ return imports;
1627
+ }
1628
+ resolveImport(specifier, fromFile) {
1629
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
1630
+ if (!this.compilerOptions?.paths || !Object.keys(this.compilerOptions.paths).length) return null;
1631
+ }
1632
+ if (!this.compilerOptions || !this.moduleResolutionCache) return null;
1633
+ const resolved = typescript.resolveModuleName(specifier, fromFile, this.compilerOptions, this.sys, this.moduleResolutionCache);
1634
+ if (resolved.resolvedModule) {
1635
+ const resolvedPath = resolved.resolvedModule.resolvedFileName;
1636
+ if (resolved.resolvedModule.isExternalLibraryImport) return null;
1637
+ if (resolvedPath.endsWith(".d.ts")) {
1638
+ const sourcePath = resolvedPath.replace(/\.d\.ts$/, ".ts");
1639
+ if (this.sys.fileExists(sourcePath)) return sourcePath;
1640
+ return null;
1641
+ }
1642
+ return resolvedPath;
1643
+ }
1644
+ return null;
1645
+ }
1646
+ isExternalModule(filePath) {
1647
+ return filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\");
1648
+ }
1649
+ isSourceFile(filePath) {
1650
+ if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) return false;
1651
+ if (filePath.endsWith(".d.ts")) return false;
1652
+ if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
1653
+ if (filePath.includes("/__test__/") || filePath.includes("\\__test__\\")) return false;
1654
+ if (filePath.includes("/__tests__/") || filePath.includes("\\__tests__\\")) return false;
1655
+ const excludePatterns = this.options.excludePatterns ?? [];
1656
+ for (const pattern of excludePatterns)if (filePath.includes(pattern)) return false;
1657
+ return true;
1658
+ }
1659
+ static fromEntries(entryPaths, options) {
1660
+ const graph = new ImportGraph(options);
1661
+ return graph.traceFromEntries(entryPaths);
1662
+ }
1663
+ static fromPackageExports(packageJsonPath, options) {
1664
+ const graph = new ImportGraph(options);
1665
+ return graph.traceFromPackageExports(packageJsonPath);
1666
+ }
1667
+ }
1668
+ function formatLintResults(results, cwd) {
1669
+ if (0 === results.messages.length) return "";
1670
+ const lines = [];
1671
+ const messagesByFile = new Map();
1672
+ for (const msg of results.messages){
1673
+ const existing = messagesByFile.get(msg.filePath) ?? [];
1674
+ existing.push(msg);
1675
+ messagesByFile.set(msg.filePath, existing);
1676
+ }
1677
+ for (const [filePath, messages] of messagesByFile){
1678
+ lines.push(picocolors.underline(picocolors.cyan((0, external_node_path_.relative)(cwd, filePath))));
1679
+ for (const msg of messages){
1680
+ const location = picocolors.dim(`${msg.line}:${msg.column}`);
1681
+ const severityColor = 2 === msg.severity ? picocolors.red : picocolors.yellow;
1682
+ const severityLabel = 2 === msg.severity ? "error" : "warning";
1683
+ const rule = msg.ruleId ? picocolors.dim(`(${msg.ruleId})`) : "";
1684
+ lines.push(` ${location} ${severityColor(severityLabel)} ${msg.message} ${rule}`);
1685
+ }
1686
+ lines.push("");
1687
+ }
1688
+ const errorText = 1 === results.errorCount ? "error" : "errors";
1689
+ const warningText = 1 === results.warningCount ? "warning" : "warnings";
1690
+ const summary = results.errorCount > 0 ? picocolors.red(`${results.errorCount} ${errorText}`) : picocolors.yellow(`${results.warningCount} ${warningText}`);
1691
+ lines.push(summary);
1692
+ return lines.join("\n");
1693
+ }
1694
+ function discoverFilesToLint(options, cwd) {
1695
+ if (options.include && options.include.length > 0) return {
1696
+ files: options.include,
1697
+ errors: [],
1698
+ isGlobPattern: true
1699
+ };
1700
+ const graph = new ImportGraph({
1701
+ rootDir: cwd
1702
+ });
1703
+ const packageJsonPath = (0, external_node_path_.join)(cwd, "package.json");
1704
+ const result = graph.traceFromPackageExports(packageJsonPath);
1705
+ return {
1706
+ files: result.files,
1707
+ errors: result.errors,
1708
+ isGlobPattern: false
1709
+ };
1710
+ }
1711
+ async function runTsDocLint(options, cwd) {
1712
+ const tsdocOptions = options.tsdoc ?? {};
1713
+ const persistConfig = options.persistConfig;
1714
+ const shouldPersist = TsDocConfigBuilder.shouldPersist(persistConfig);
1715
+ const tsdocConfigOutputPath = TsDocConfigBuilder.getConfigPath(persistConfig, cwd);
1716
+ const tsdocConfigPath = await TsDocConfigBuilder.writeConfigFile(tsdocOptions, (0, external_node_path_.dirname)(tsdocConfigOutputPath));
1717
+ let ESLint;
1718
+ let tsParserModule;
1719
+ let tsdocPluginModule;
1720
+ const missingPackages = [];
1721
+ try {
1722
+ const eslintModule = await import("eslint");
1723
+ ESLint = eslintModule.ESLint;
1724
+ } catch {
1725
+ missingPackages.push("eslint");
1726
+ }
1727
+ try {
1728
+ tsParserModule = await import("@typescript-eslint/parser");
1729
+ } catch {
1730
+ missingPackages.push("@typescript-eslint/parser");
1731
+ }
1732
+ try {
1733
+ tsdocPluginModule = await import("eslint-plugin-tsdoc");
1734
+ } catch {
1735
+ missingPackages.push("eslint-plugin-tsdoc");
1736
+ }
1737
+ if (missingPackages.length > 0 || !ESLint) throw new Error(`TsDocLintPlugin requires: ${missingPackages.join(", ")}\nInstall with: pnpm add -D ${missingPackages.join(" ")}`);
1738
+ const tsParser = tsParserModule.default ?? tsParserModule;
1739
+ const tsdocPlugin = tsdocPluginModule.default ?? tsdocPluginModule;
1740
+ const discovery = discoverFilesToLint(options, cwd);
1741
+ if (0 === discovery.files.length) return {
1742
+ results: {
1743
+ errorCount: 0,
1744
+ warningCount: 0,
1745
+ messages: []
1746
+ },
1747
+ tsdocConfigPath: shouldPersist ? tsdocConfigPath : void 0,
1748
+ discoveryErrors: discovery.errors
1749
+ };
1750
+ let eslintConfig;
1751
+ let filesToLint;
1752
+ if (discovery.isGlobPattern) {
1753
+ const includePatterns = discovery.files;
1754
+ filesToLint = includePatterns.filter((p)=>!p.startsWith("!"));
1755
+ eslintConfig = [
1756
+ {
1757
+ ignores: [
1758
+ "**/node_modules/**",
1759
+ "**/dist/**",
1760
+ "**/coverage/**"
1761
+ ]
1762
+ },
1763
+ {
1764
+ files: filesToLint,
1765
+ ignores: includePatterns.filter((p)=>p.startsWith("!")).map((p)=>p.slice(1)),
1766
+ languageOptions: {
1767
+ parser: tsParser
1768
+ },
1769
+ plugins: {
1770
+ tsdoc: tsdocPlugin
1771
+ },
1772
+ rules: {
1773
+ "tsdoc/syntax": "error"
1774
+ }
1775
+ }
1776
+ ];
1777
+ } else {
1778
+ filesToLint = discovery.files;
1779
+ eslintConfig = [
1780
+ {
1781
+ ignores: [
1782
+ "**/node_modules/**",
1783
+ "**/dist/**",
1784
+ "**/coverage/**"
1785
+ ]
1786
+ },
1787
+ {
1788
+ files: [
1789
+ "**/*.ts",
1790
+ "**/*.tsx"
1791
+ ],
1792
+ languageOptions: {
1793
+ parser: tsParser
1794
+ },
1795
+ plugins: {
1796
+ tsdoc: tsdocPlugin
1797
+ },
1798
+ rules: {
1799
+ "tsdoc/syntax": "error"
1800
+ }
1801
+ }
1802
+ ];
1803
+ }
1804
+ const eslint = new ESLint({
1805
+ cwd,
1806
+ overrideConfigFile: true,
1807
+ overrideConfig: eslintConfig
1808
+ });
1809
+ const eslintResults = await eslint.lintFiles(filesToLint);
1810
+ const messages = [];
1811
+ let errorCount = 0;
1812
+ let warningCount = 0;
1813
+ for (const result of eslintResults)for (const msg of result.messages){
1814
+ messages.push({
1815
+ filePath: result.filePath,
1816
+ line: msg.line,
1817
+ column: msg.column,
1818
+ message: msg.message,
1819
+ ruleId: msg.ruleId,
1820
+ severity: msg.severity
1821
+ });
1822
+ if (2 === msg.severity) errorCount++;
1823
+ else warningCount++;
1824
+ }
1825
+ return {
1826
+ results: {
1827
+ errorCount,
1828
+ warningCount,
1829
+ messages
1830
+ },
1831
+ tsdocConfigPath: shouldPersist ? tsdocConfigPath : void 0,
1832
+ discoveryErrors: discovery.errors.length > 0 ? discovery.errors : void 0
1833
+ };
1834
+ }
1835
+ async function cleanupTsDocConfig(configPath) {
1836
+ if (!configPath) return;
1837
+ try {
1838
+ const { unlink } = await import("node:fs/promises");
1839
+ await unlink(configPath);
1840
+ } catch {}
1841
+ }
1842
+ const TsDocLintPlugin = (options = {})=>{
1843
+ const { enabled = true } = options;
1844
+ let tempTsDocConfigPath;
1845
+ return {
1846
+ name: "tsdoc-lint-plugin",
1847
+ setup (api) {
1848
+ if (!enabled) return;
1849
+ api.onBeforeBuild(async ()=>{
1850
+ const cwd = api.context.rootPath;
1851
+ const isCI = TsDocConfigBuilder.isCI();
1852
+ const onError = options.onError ?? (isCI ? "throw" : "error");
1853
+ core_logger.info(`${picocolors.dim("[tsdoc-lint]")} Validating TSDoc comments...`);
1854
+ try {
1855
+ const { results, tsdocConfigPath, discoveryErrors } = await runTsDocLint(options, cwd);
1856
+ if (discoveryErrors && discoveryErrors.length > 0) for (const error of discoveryErrors)core_logger.warn(`${picocolors.dim("[tsdoc-lint]")} ${error.message}`);
1857
+ if (!TsDocConfigBuilder.shouldPersist(options.persistConfig)) tempTsDocConfigPath = tsdocConfigPath;
1858
+ if (0 === results.errorCount && 0 === results.warningCount) return void core_logger.info(`${picocolors.dim("[tsdoc-lint]")} ${picocolors.green("All TSDoc comments are valid")}`);
1859
+ const formatted = formatLintResults(results, cwd);
1860
+ if (results.errorCount > 0) if ("throw" === onError) throw new Error(`TSDoc validation failed:\n${formatted}`);
1861
+ else if ("error" === onError) core_logger.error(`${picocolors.dim("[tsdoc-lint]")} TSDoc validation errors:\n${formatted}`);
1862
+ else core_logger.warn(`${picocolors.dim("[tsdoc-lint]")} TSDoc validation warnings:\n${formatted}`);
1863
+ else if (results.warningCount > 0) core_logger.warn(`${picocolors.dim("[tsdoc-lint]")} TSDoc validation warnings:\n${formatted}`);
1864
+ } catch (error) {
1865
+ await cleanupTsDocConfig(tempTsDocConfigPath);
1866
+ throw error;
1867
+ }
1868
+ });
1869
+ api.onCloseBuild(async ()=>{
1870
+ await cleanupTsDocConfig(tempTsDocConfigPath);
1871
+ });
1872
+ }
1873
+ };
1874
+ };
1443
1875
  /* v8 ignore next -- @preserve */ class NodeLibraryBuilder {
1444
1876
  static DEFAULT_OPTIONS = {
1445
1877
  entry: void 0,
@@ -1453,7 +1885,8 @@ const PackageJsonTransformPlugin = (options = {})=>{
1453
1885
  tsconfigPath: void 0,
1454
1886
  externals: [],
1455
1887
  dtsBundledPackages: void 0,
1456
- transformFiles: void 0
1888
+ transformFiles: void 0,
1889
+ tsdocLint: void 0
1457
1890
  };
1458
1891
  static mergeOptions(options = {}) {
1459
1892
  const merged = {
@@ -1487,6 +1920,11 @@ const PackageJsonTransformPlugin = (options = {})=>{
1487
1920
  const options = NodeLibraryBuilder.mergeOptions(opts);
1488
1921
  const VERSION = await packageJsonVersion();
1489
1922
  const plugins = [];
1923
+ if (options.tsdocLint) {
1924
+ const lintOptions = true === options.tsdocLint ? {} : options.tsdocLint;
1925
+ if (!lintOptions.tsdoc && "object" == typeof options.apiModel && options.apiModel.tsdoc) lintOptions.tsdoc = options.apiModel.tsdoc;
1926
+ plugins.push(TsDocLintPlugin(lintOptions));
1927
+ }
1490
1928
  if ("dev" === target || "npm" === target) {
1491
1929
  if (!options.entry) plugins.push(AutoEntryPlugin({
1492
1930
  exportsAsIndexes: options.exportsAsIndexes
@@ -1565,4 +2003,4 @@ const PackageJsonTransformPlugin = (options = {})=>{
1565
2003
  });
1566
2004
  }
1567
2005
  }
1568
- export { AutoEntryPlugin, DtsPlugin, FilesArrayPlugin, NodeLibraryBuilder, PackageJsonTransformPlugin, TsDocConfigBuilder };
2006
+ export { AutoEntryPlugin, DtsPlugin, FilesArrayPlugin, ImportGraph, NodeLibraryBuilder, PackageJsonTransformPlugin, TsDocConfigBuilder, TsDocLintPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/rslib-builder",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "RSlib-based build system for Node.js libraries with automatic package.json transformation, TypeScript declaration bundling, and multi-target support",
6
6
  "homepage": "https://github.com/savvy-web/rslib-builder",
@@ -27,6 +27,7 @@
27
27
  "@microsoft/tsdoc": "^0.16.0",
28
28
  "@microsoft/tsdoc-config": "^0.18.0",
29
29
  "@pnpm/exportable-manifest": "^1000.3.1",
30
+ "deep-equal": "^2.2.3",
30
31
  "glob": "^13.0.0",
31
32
  "picocolors": "^1.1.1",
32
33
  "sort-package-json": "^3.6.0",
@@ -38,7 +39,10 @@
38
39
  "@microsoft/api-extractor": "^7.55.2",
39
40
  "@rslib/core": "^0.19.2",
40
41
  "@types/node": "^25.0.9",
42
+ "@typescript-eslint/parser": "^8.0.0",
41
43
  "@typescript/native-preview": "^7.0.0-dev.20260120.1",
44
+ "eslint": "^9.0.0",
45
+ "eslint-plugin-tsdoc": "^0.5.0",
42
46
  "typescript": "^5.9.3"
43
47
  },
44
48
  "peerDependenciesMeta": {
@@ -48,9 +52,18 @@
48
52
  "@rslib/core": {
49
53
  "optional": false
50
54
  },
55
+ "@typescript-eslint/parser": {
56
+ "optional": true
57
+ },
51
58
  "@typescript/native-preview": {
52
59
  "optional": false
53
60
  },
61
+ "eslint": {
62
+ "optional": true
63
+ },
64
+ "eslint-plugin-tsdoc": {
65
+ "optional": true
66
+ },
54
67
  "typescript": {
55
68
  "optional": false
56
69
  }