@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.
- package/README.md +70 -18
- package/index.d.ts +567 -0
- package/index.js +445 -7
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
|
|
7
|
-
Build modern ESM Node.js libraries with
|
|
8
|
-
declarations, package.json transformations, and PNPM workspace
|
|
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
|
|
23
|
-
|
|
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. **
|
|
106
|
-
2. **
|
|
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. **
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
##
|
|
197
|
+
## Links
|
|
146
198
|
|
|
147
|
-
[
|
|
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
|
-
##
|
|
210
|
+
## License
|
|
155
211
|
|
|
156
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
"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
|
}
|