@savvy-web/rslib-builder 0.11.0 → 0.12.1

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 CHANGED
@@ -16,6 +16,8 @@ resolution automatically.
16
16
  Extractor for bundled, clean public API declarations
17
17
  - **Production-Ready Transforms** - Converts `.ts` exports to `.js`, resolves
18
18
  PNPM `catalog:` and `workspace:` references, generates files array
19
+ - **Bundled or Bundleless** - Choose single-file bundles per entry or
20
+ bundleless mode that preserves your source file structure
19
21
  - **Multi-Target Builds** - Separate dev (with source maps) and npm (optimized)
20
22
  outputs from a single configuration
21
23
  - **TSDoc Validation** - Pre-build documentation validation with automatic
package/index.d.ts CHANGED
@@ -62,10 +62,9 @@ import ts from 'typescript';
62
62
  * API model generation is enabled by default. To disable, set `apiModel: false`.
63
63
  * Providing an options object implicitly enables API model generation.
64
64
  *
65
- * API models are only generated for the main "index" entry point (the "." export).
66
- * Additional entry points like "./hooks" or "./utils" do not generate separate API models.
67
- * This prevents multiple conflicting API models and ensures a single source of truth
68
- * for package documentation.
65
+ * When a package has multiple entry points, API Extractor runs for each entry and the
66
+ * resulting per-entry models are merged into a single API model with multiple
67
+ * EntryPoint members. Canonical references are scoped per entry point.
69
68
  *
70
69
  * @public
71
70
  */
@@ -222,6 +221,18 @@ export declare interface AutoEntryPluginOptions {
222
221
  * @defaultValue false
223
222
  */
224
223
  exportsAsIndexes?: boolean;
224
+ /**
225
+ * When enabled, uses import graph analysis to discover all reachable files
226
+ * from entry points, and sets RSlib's source.entry to the traced file list
227
+ * instead of named entries.
228
+ *
229
+ * @remarks
230
+ * Named entries are still exposed via the `entrypoints` map for DtsPlugin
231
+ * and PackageJsonTransformPlugin to use.
232
+ *
233
+ * @defaultValue false
234
+ */
235
+ bundleless?: boolean;
225
236
  }
226
237
 
227
238
  /**
@@ -515,24 +526,14 @@ export declare interface ExtractedEntries {
515
526
  * Entry name to TypeScript source path mapping.
516
527
  */
517
528
  entries: Record<string, string>;
529
+ /**
530
+ * Entry name to original export key mapping.
531
+ * Maps entry names back to the original package.json export path
532
+ * (e.g., `"nested-one"` → `"./nested/one"`).
533
+ */
534
+ exportPaths: Record<string, string>;
518
535
  }
519
536
 
520
- /**
521
- * Extracts TypeScript entry points from package.json (functional interface).
522
- *
523
- * @remarks
524
- * This is a convenience function that creates an EntryExtractor instance
525
- * and extracts entries in one call. For repeated use, consider creating
526
- * an EntryExtractor instance directly.
527
- *
528
- * @param packageJson - The package.json object to extract entries from
529
- * @param options - Configuration options for entry extraction
530
- * @returns Object containing the extracted entries
531
- *
532
- * @public
533
- */
534
- export declare function extractEntriesFromPackageJson(packageJson: PackageJson, options?: EntryExtractorOptions): ExtractedEntries;
535
-
536
537
  /**
537
538
  * Plugin to manage the `files` array in package.json for npm publishing.
538
539
  *
@@ -1162,6 +1163,7 @@ export declare class NodeLibraryBuilder {
1162
1163
  targets: ("dev" | "npm")[];
1163
1164
  externals: never[];
1164
1165
  apiModel: true;
1166
+ bundle: true;
1165
1167
  };
1166
1168
  /**
1167
1169
  * Merges user-provided options with default options.
@@ -1480,6 +1482,19 @@ export declare interface NodeLibraryBuilderOptions {
1480
1482
  * ```
1481
1483
  */
1482
1484
  apiModel?: ApiModelOptions | boolean;
1485
+ /**
1486
+ * Whether to bundle JavaScript output into single files per entry point.
1487
+ *
1488
+ * @remarks
1489
+ * - `true` (default): RSlib bundles JS into single files per entry (current behavior)
1490
+ * - `false`: RSlib runs in bundleless mode, preserving file structure for JS output.
1491
+ * DTS is still bundled per entry via API Extractor (hybrid mode).
1492
+ * When `apiModel` is enabled with multiple entries, per-entry API models are
1493
+ * merged into a single `api.model.json` with multiple EntryPoint members.
1494
+ *
1495
+ * @defaultValue true
1496
+ */
1497
+ bundle?: boolean;
1483
1498
  }
1484
1499
 
1485
1500
  /**
@@ -2901,6 +2916,16 @@ export declare class TsconfigResolver {
2901
2916
  * @defaultValue `true`
2902
2917
  */
2903
2918
  persistConfig?: boolean | PathLike;
2919
+ /**
2920
+ * Whether to run lint per entry point individually with per-entry logging.
2921
+ *
2922
+ * @remarks
2923
+ * When enabled, each entry point is linted separately and results are
2924
+ * logged per entry for better visibility in bundleless mode.
2925
+ *
2926
+ * @defaultValue false
2927
+ */
2928
+ perEntry?: boolean;
2904
2929
  }
2905
2930
 
2906
2931
  /**
package/index.js CHANGED
@@ -6,10 +6,10 @@ import { access, copyFile, mkdir, readFile, readdir, rm, stat, unlink as promise
6
6
  import { logger as core_logger } from "@rsbuild/core";
7
7
  import picocolors from "picocolors";
8
8
  import { getCatalogs, getWorkspaceManagerAndRoot, getWorkspaceManagerRoot } from "workspace-tools";
9
+ import typescript, { ImportsNotUsedAsValues, JsxEmit, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, NewLineKind, ScriptTarget, createCompilerHost, findConfigFile, formatDiagnostic, parseJsonConfigFileContent, readConfigFile, sys } from "typescript";
9
10
  import { spawn } from "node:child_process";
10
11
  import { StandardTags, Standardization, TSDocTagSyntaxKind } from "@microsoft/tsdoc";
11
12
  import deep_equal from "deep-equal";
12
- import typescript, { ImportsNotUsedAsValues, JsxEmit, ModuleDetectionKind, ModuleKind, ModuleResolutionKind, NewLineKind, ScriptTarget, createCompilerHost, findConfigFile, formatDiagnostic, parseJsonConfigFileContent, readConfigFile, sys } from "typescript";
13
13
  import { createRequire } from "node:module";
14
14
  import { inspect } from "node:util";
15
15
  import sort_package_json from "sort-package-json";
@@ -86,16 +86,21 @@ class EntryExtractor {
86
86
  }
87
87
  extract(packageJson) {
88
88
  const entries = {};
89
- this.extractFromExports(packageJson.exports, entries);
89
+ const exportPaths = {};
90
+ this.extractFromExports(packageJson.exports, entries, exportPaths);
90
91
  this.extractFromBin(packageJson.bin, entries);
91
92
  return {
92
- entries
93
+ entries,
94
+ exportPaths
93
95
  };
94
96
  }
95
- extractFromExports(exports, entries) {
97
+ extractFromExports(exports, entries, exportPaths) {
96
98
  if (!exports) return;
97
99
  if ("string" == typeof exports) {
98
- if (this.isTypeScriptFile(exports)) entries.index = exports;
100
+ if (this.isTypeScriptFile(exports)) {
101
+ entries.index = exports;
102
+ exportPaths.index = ".";
103
+ }
99
104
  return;
100
105
  }
101
106
  if ("object" != typeof exports) return;
@@ -107,6 +112,7 @@ class EntryExtractor {
107
112
  if (!this.isTypeScriptFile(resolvedPath)) continue;
108
113
  const entryName = this.createEntryName(key);
109
114
  entries[entryName] = resolvedPath;
115
+ exportPaths[entryName] = key;
110
116
  }
111
117
  }
112
118
  extractFromBin(bin, entries) {
@@ -144,10 +150,6 @@ class EntryExtractor {
144
150
  return withoutPrefix.replace(/\//g, "-");
145
151
  }
146
152
  }
147
- function extractEntriesFromPackageJson(packageJson, options) {
148
- const extractor = new EntryExtractor(options);
149
- return extractor.extract(packageJson);
150
- }
151
153
  async function fileExistAsync(assetName) {
152
154
  const assetPath = join(process.cwd(), assetName);
153
155
  const assetExists = !!await stat(assetPath).catch(()=>false);
@@ -179,6 +181,223 @@ function getApiExtractorPath() {
179
181
  }
180
182
  throw new Error("API Extractor bundling requires @microsoft/api-extractor to be installed.\nInstall it with: pnpm add -D @microsoft/api-extractor");
181
183
  }
184
+ class ImportGraph {
185
+ options;
186
+ sys;
187
+ program = null;
188
+ compilerOptions = null;
189
+ moduleResolutionCache = null;
190
+ constructor(options){
191
+ this.options = options;
192
+ this.sys = options.sys ?? typescript.sys;
193
+ }
194
+ traceFromEntries(entryPaths) {
195
+ const errors = [];
196
+ const visited = new Set();
197
+ const entries = [];
198
+ const initResult = this.initializeProgram();
199
+ if (!initResult.success) return {
200
+ files: [],
201
+ entries: [],
202
+ errors: [
203
+ initResult.error
204
+ ]
205
+ };
206
+ for (const entryPath of entryPaths){
207
+ const absolutePath = this.resolveEntryPath(entryPath);
208
+ if (!this.sys.fileExists(absolutePath)) {
209
+ errors.push({
210
+ type: "entry_not_found",
211
+ message: `Entry file not found: ${entryPath}`,
212
+ path: absolutePath
213
+ });
214
+ continue;
215
+ }
216
+ entries.push(absolutePath);
217
+ this.traceImports(absolutePath, visited, errors);
218
+ }
219
+ const files = Array.from(visited).filter((file)=>this.isSourceFile(file));
220
+ return {
221
+ files: files.sort(),
222
+ entries,
223
+ errors
224
+ };
225
+ }
226
+ traceFromPackageExports(packageJsonPath) {
227
+ const absolutePath = this.resolveEntryPath(packageJsonPath);
228
+ let packageJson;
229
+ try {
230
+ const content = this.sys.readFile(absolutePath);
231
+ if (!content) return {
232
+ files: [],
233
+ entries: [],
234
+ errors: [
235
+ {
236
+ type: "package_json_not_found",
237
+ message: `Failed to read package.json: File not found at ${absolutePath}`,
238
+ path: absolutePath
239
+ }
240
+ ]
241
+ };
242
+ packageJson = JSON.parse(content);
243
+ } catch (error) {
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ return {
246
+ files: [],
247
+ entries: [],
248
+ errors: [
249
+ {
250
+ type: "package_json_parse_error",
251
+ message: `Failed to parse package.json: ${message}`,
252
+ path: absolutePath
253
+ }
254
+ ]
255
+ };
256
+ }
257
+ const extractor = new EntryExtractor();
258
+ const { entries } = extractor.extract(packageJson);
259
+ const packageDir = dirname(absolutePath);
260
+ const entryPaths = Object.values(entries).map((p)=>external_node_path_resolve(packageDir, p));
261
+ return this.traceFromEntries(entryPaths);
262
+ }
263
+ initializeProgram() {
264
+ if (this.program) return {
265
+ success: true
266
+ };
267
+ const configPath = this.findTsConfig();
268
+ if (!configPath) return {
269
+ success: false,
270
+ error: {
271
+ type: "tsconfig_not_found",
272
+ message: `No tsconfig.json found in ${this.options.rootDir}`,
273
+ path: this.options.rootDir
274
+ }
275
+ };
276
+ const configFile = typescript.readConfigFile(configPath, (path)=>this.sys.readFile(path));
277
+ if (configFile.error) {
278
+ const message = typescript.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
279
+ return {
280
+ success: false,
281
+ error: {
282
+ type: "tsconfig_read_error",
283
+ message: `Failed to read tsconfig.json: ${message}`,
284
+ path: configPath
285
+ }
286
+ };
287
+ }
288
+ const parsed = typescript.parseJsonConfigFileContent(configFile.config, this.sys, dirname(configPath));
289
+ if (parsed.errors.length > 0) {
290
+ const messages = parsed.errors.map((e)=>typescript.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
291
+ return {
292
+ success: false,
293
+ error: {
294
+ type: "tsconfig_parse_error",
295
+ message: `Failed to parse tsconfig.json: ${messages}`,
296
+ path: configPath
297
+ }
298
+ };
299
+ }
300
+ this.compilerOptions = parsed.options;
301
+ this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
302
+ const host = typescript.createCompilerHost(this.compilerOptions, true);
303
+ host.getCurrentDirectory = ()=>this.options.rootDir;
304
+ this.program = typescript.createProgram([], this.compilerOptions, host);
305
+ return {
306
+ success: true
307
+ };
308
+ }
309
+ findTsConfig() {
310
+ if (this.options.tsconfigPath) {
311
+ const customPath = isAbsolute(this.options.tsconfigPath) ? this.options.tsconfigPath : external_node_path_resolve(this.options.rootDir, this.options.tsconfigPath);
312
+ if (this.sys.fileExists(customPath)) return customPath;
313
+ return null;
314
+ }
315
+ const configPath = typescript.findConfigFile(this.options.rootDir, (path)=>this.sys.fileExists(path));
316
+ return configPath ?? null;
317
+ }
318
+ resolveEntryPath(entryPath) {
319
+ if (isAbsolute(entryPath)) return normalize(entryPath);
320
+ return normalize(external_node_path_resolve(this.options.rootDir, entryPath));
321
+ }
322
+ traceImports(filePath, visited, errors) {
323
+ const normalizedPath = normalize(filePath);
324
+ if (visited.has(normalizedPath)) return;
325
+ if (this.isExternalModule(normalizedPath)) return;
326
+ visited.add(normalizedPath);
327
+ const content = this.sys.readFile(normalizedPath);
328
+ if (!content) return void errors.push({
329
+ type: "file_read_error",
330
+ message: `Failed to read file: ${normalizedPath}`,
331
+ path: normalizedPath
332
+ });
333
+ const sourceFile = typescript.createSourceFile(normalizedPath, content, typescript.ScriptTarget.Latest, true);
334
+ const imports = this.extractImports(sourceFile);
335
+ for (const importPath of imports){
336
+ const resolved = this.resolveImport(importPath, normalizedPath);
337
+ if (resolved) this.traceImports(resolved, visited, errors);
338
+ }
339
+ }
340
+ extractImports(sourceFile) {
341
+ const imports = [];
342
+ const visit = (node)=>{
343
+ if (typescript.isImportDeclaration(node)) {
344
+ const specifier = node.moduleSpecifier;
345
+ if (typescript.isStringLiteral(specifier)) imports.push(specifier.text);
346
+ } else if (typescript.isExportDeclaration(node)) {
347
+ const specifier = node.moduleSpecifier;
348
+ if (specifier && typescript.isStringLiteral(specifier)) imports.push(specifier.text);
349
+ } else if (typescript.isCallExpression(node)) {
350
+ const expression = node.expression;
351
+ if (expression.kind === typescript.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
352
+ const arg = node.arguments[0];
353
+ if (arg && typescript.isStringLiteral(arg)) imports.push(arg.text);
354
+ }
355
+ }
356
+ typescript.forEachChild(node, visit);
357
+ };
358
+ visit(sourceFile);
359
+ return imports;
360
+ }
361
+ resolveImport(specifier, fromFile) {
362
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
363
+ if (!this.compilerOptions?.paths || !Object.keys(this.compilerOptions.paths).length) return null;
364
+ }
365
+ if (!this.compilerOptions || !this.moduleResolutionCache) return null;
366
+ const resolved = typescript.resolveModuleName(specifier, fromFile, this.compilerOptions, this.sys, this.moduleResolutionCache);
367
+ if (resolved.resolvedModule) {
368
+ const resolvedPath = resolved.resolvedModule.resolvedFileName;
369
+ if (resolved.resolvedModule.isExternalLibraryImport) return null;
370
+ if (resolvedPath.endsWith(".d.ts")) {
371
+ const sourcePath = resolvedPath.replace(/\.d\.ts$/, ".ts");
372
+ if (this.sys.fileExists(sourcePath)) return sourcePath;
373
+ return null;
374
+ }
375
+ return resolvedPath;
376
+ }
377
+ return null;
378
+ }
379
+ isExternalModule(filePath) {
380
+ return filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\");
381
+ }
382
+ isSourceFile(filePath) {
383
+ if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) return false;
384
+ if (filePath.endsWith(".d.ts")) return false;
385
+ if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
386
+ if (filePath.includes("/__test__/") || filePath.includes("\\__test__\\")) return false;
387
+ if (filePath.includes("/__tests__/") || filePath.includes("\\__tests__\\")) return false;
388
+ const excludePatterns = this.options.excludePatterns ?? [];
389
+ for (const pattern of excludePatterns)if (filePath.includes(pattern)) return false;
390
+ return true;
391
+ }
392
+ static fromEntries(entryPaths, options) {
393
+ const graph = new ImportGraph(options);
394
+ return graph.traceFromEntries(entryPaths);
395
+ }
396
+ static fromPackageExports(packageJsonPath, options) {
397
+ const graph = new ImportGraph(options);
398
+ return graph.traceFromPackageExports(packageJsonPath);
399
+ }
400
+ }
182
401
  const AutoEntryPlugin = (options)=>{
183
402
  const buildStateMap = new WeakMap();
184
403
  return {
@@ -208,15 +427,16 @@ const AutoEntryPlugin = (options)=>{
208
427
  try {
209
428
  const packageJsonContent = await readFile(assetPath, "utf-8");
210
429
  const packageJson = JSON.parse(packageJsonContent);
211
- const { entries } = extractEntriesFromPackageJson(packageJson, options?.exportsAsIndexes != null ? {
430
+ const extractorOptions = options?.exportsAsIndexes != null ? {
212
431
  exportsAsIndexes: options.exportsAsIndexes
213
- } : void 0);
432
+ } : void 0;
433
+ const { entries } = new EntryExtractor(extractorOptions).extract(packageJson);
214
434
  if (options?.exportsAsIndexes && packageJson.exports) {
215
435
  const exports = packageJson.exports;
216
436
  if ("object" == typeof exports && !Array.isArray(exports)) for (const pkgExportKey of Object.keys(exports)){
217
437
  if ("./package.json" === pkgExportKey) continue;
218
438
  const normalizedExportKey = pkgExportKey.replace(/^\.\//, "");
219
- for (const [entryName] of Object.entries(entries)){
439
+ for (const entryName of Object.keys(entries)){
220
440
  const normalizedEntryName = entryName.replace(/\/index$/, "");
221
441
  if ("." === pkgExportKey && "index" === entryName || normalizedExportKey === normalizedEntryName) {
222
442
  const outputPath = `./${entryName}.js`;
@@ -232,7 +452,26 @@ const AutoEntryPlugin = (options)=>{
232
452
  }
233
453
  if (Object.keys(entries).length > 0) {
234
454
  const environments = Object.entries(config?.environments ?? {});
235
- environments.forEach(([_env, lib])=>{
455
+ if (options?.bundleless) {
456
+ const cwd = process.cwd();
457
+ const graph = new ImportGraph({
458
+ rootDir: cwd
459
+ });
460
+ const entrySourcePaths = Object.values(entries);
461
+ const result = graph.traceFromEntries(entrySourcePaths);
462
+ const tracedEntries = {};
463
+ for (const file of result.files){
464
+ const relPath = relative(cwd, file);
465
+ tracedEntries[relPath] = `./${relPath}`;
466
+ }
467
+ log.global.info(`bundleless: traced ${Object.keys(tracedEntries).length} files from ${entrySourcePaths.length} entries`);
468
+ environments.forEach(([_env, lib])=>{
469
+ lib.source = {
470
+ ...lib.source,
471
+ entry: tracedEntries
472
+ };
473
+ });
474
+ } else environments.forEach(([_env, lib])=>{
236
475
  lib.source = {
237
476
  ...lib.source,
238
477
  entry: {
@@ -774,7 +1013,7 @@ class TsDocConfigBuilder {
774
1013
  const tsdocConfig = {
775
1014
  $schema: "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
776
1015
  noStandardTags: !useStandardTags,
777
- reportUnsupportedHtmlElements: false
1016
+ reportUnsupportedHtmlElements: true
778
1017
  };
779
1018
  if (tagDefinitions.length > 0) tsdocConfig.tagDefinitions = tagDefinitions;
780
1019
  if (Object.keys(supportForTags).length > 0) tsdocConfig.supportForTags = supportForTags;
@@ -872,19 +1111,22 @@ async function collectDtsFiles(dir, baseDir = dir) {
872
1111
  await walk(dir);
873
1112
  return files;
874
1113
  }
1114
+ function resolveTsdocMetadataFilename(apiModel) {
1115
+ const option = "object" == typeof apiModel ? apiModel.tsdocMetadata : void 0;
1116
+ return "object" == typeof option && option.filename ? option.filename : "tsdoc-metadata.json";
1117
+ }
875
1118
  async function bundleDtsFiles(options) {
876
1119
  const { cwd, tempDtsDir, tempOutputDir, tsconfigPath, bundledPackages, entryPoints, banner, footer, apiModel } = options;
877
1120
  const bundledFiles = new Map();
878
- let apiModelPath;
1121
+ const apiModelPaths = new Map();
879
1122
  let tsdocMetadataPath;
880
1123
  const apiModelEnabled = true === apiModel || "object" == typeof apiModel;
881
- const apiModelFilename = "object" == typeof apiModel && apiModel.filename ? apiModel.filename : "api.json";
882
1124
  const tsdocOptions = "object" == typeof apiModel ? apiModel.tsdoc : void 0;
883
1125
  const tsdocMetadataOption = "object" == typeof apiModel ? apiModel.tsdocMetadata : void 0;
884
1126
  const tsdocWarnings = tsdocOptions?.warnings ?? (TsDocConfigBuilder.isCI() ? "fail" : "log");
885
1127
  const forgottenExports = ("object" == typeof apiModel ? apiModel.forgottenExports : void 0) ?? (TsDocConfigBuilder.isCI() ? "error" : "include");
886
1128
  const tsdocMetadataEnabled = apiModelEnabled && (void 0 === tsdocMetadataOption || true === tsdocMetadataOption || "object" == typeof tsdocMetadataOption && false !== tsdocMetadataOption.enabled);
887
- const tsdocMetadataFilename = "object" == typeof tsdocMetadataOption && tsdocMetadataOption.filename ? tsdocMetadataOption.filename : "tsdoc-metadata.json";
1129
+ const tsdocMetadataFilename = resolveTsdocMetadataFilename(apiModel);
888
1130
  getApiExtractorPath();
889
1131
  const lintConfig = "object" == typeof tsdocOptions?.lint ? tsdocOptions.lint : void 0;
890
1132
  const persistConfig = lintConfig?.persistConfig;
@@ -916,9 +1158,10 @@ async function bundleDtsFiles(options) {
916
1158
  }
917
1159
  const outputFileName = `${entryName}.d.ts`;
918
1160
  const tempBundledPath = join(tempOutputDir, outputFileName);
919
- const generateApiModel = apiModelEnabled && "index" === entryName;
920
- const tempApiModelPath = generateApiModel ? join(tempOutputDir, apiModelFilename) : void 0;
921
- const generateTsdocMetadata = tsdocMetadataEnabled && "index" === entryName;
1161
+ const isMainEntry = "index" === entryName || 1 === entryPoints.size;
1162
+ const generateApiModel = apiModelEnabled;
1163
+ const tempApiModelPath = generateApiModel ? join(tempOutputDir, `${entryName}.api.json`) : void 0;
1164
+ const generateTsdocMetadata = tsdocMetadataEnabled && isMainEntry;
922
1165
  const tempTsdocMetadataPath = generateTsdocMetadata ? join(tempOutputDir, tsdocMetadataFilename) : void 0;
923
1166
  const prepareOptions = {
924
1167
  configObject: {
@@ -948,7 +1191,7 @@ async function bundleDtsFiles(options) {
948
1191
  }
949
1192
  }
950
1193
  },
951
- bundledPackages: bundledPackages
1194
+ bundledPackages
952
1195
  },
953
1196
  packageJsonFullPath: join(cwd, "package.json"),
954
1197
  configObjectFullPath: void 0
@@ -1028,7 +1271,7 @@ async function bundleDtsFiles(options) {
1028
1271
  if ("error" === forgottenExports) throw new Error(`Forgotten exports detected for entry "${entryName}":\n ${forgottenMessages}`);
1029
1272
  if ("include" === forgottenExports) core_logger.warn(`Forgotten exports for entry "${entryName}":\n ${forgottenMessages}`);
1030
1273
  }
1031
- if (generateApiModel && tempApiModelPath) apiModelPath = tempApiModelPath;
1274
+ if (generateApiModel && tempApiModelPath) apiModelPaths.set(entryName, tempApiModelPath);
1032
1275
  if (generateTsdocMetadata && tempTsdocMetadataPath) tsdocMetadataPath = tempTsdocMetadataPath;
1033
1276
  if (banner || footer) {
1034
1277
  let content = await readFile(tempBundledPath, "utf-8");
@@ -1040,9 +1283,7 @@ async function bundleDtsFiles(options) {
1040
1283
  }
1041
1284
  return {
1042
1285
  bundledFiles,
1043
- ...apiModelPath && {
1044
- apiModelPath
1045
- },
1286
+ apiModelPaths,
1046
1287
  ...tsdocMetadataPath && {
1047
1288
  tsdocMetadataPath
1048
1289
  },
@@ -1054,6 +1295,45 @@ async function bundleDtsFiles(options) {
1054
1295
  function stripSourceMapComment(content) {
1055
1296
  return content.replace(/\/\/# sourceMappingURL=\S+\.d\.ts\.map\s*$/gm, "").trim();
1056
1297
  }
1298
+ function mergeApiModels(options) {
1299
+ const { perEntryModels, packageName, exportPaths } = options;
1300
+ if (0 === perEntryModels.size) throw new Error("Cannot merge zero API models");
1301
+ const firstModel = perEntryModels.values().next().value;
1302
+ const merged = JSON.parse(JSON.stringify(firstModel));
1303
+ const entryPointMembers = [];
1304
+ for (const [entryName, model] of perEntryModels){
1305
+ const entryPoints = model.members;
1306
+ if (!entryPoints || 0 === entryPoints.length) continue;
1307
+ const entryPoint = JSON.parse(JSON.stringify(entryPoints[0]));
1308
+ const exportPath = exportPaths[entryName] ?? ("index" === entryName ? "." : `./${entryName}`);
1309
+ const isMainEntry = "." === exportPath;
1310
+ if (isMainEntry) entryPointMembers.unshift(entryPoint);
1311
+ else {
1312
+ const subpath = exportPath.replace(/^\.\//, "");
1313
+ const originalPrefix = `${packageName}!`;
1314
+ const newPrefix = `${packageName}/${subpath}!`;
1315
+ entryPoint.canonicalReference = newPrefix;
1316
+ entryPoint.name = subpath;
1317
+ rewriteCanonicalReferences(entryPoint, originalPrefix, newPrefix);
1318
+ entryPointMembers.push(entryPoint);
1319
+ }
1320
+ }
1321
+ merged.members = entryPointMembers;
1322
+ return merged;
1323
+ }
1324
+ function rewriteCanonicalReferences(node, originalPrefix, newPrefix) {
1325
+ if (!node || "object" != typeof node) return;
1326
+ if (Array.isArray(node)) {
1327
+ for (const item of node)rewriteCanonicalReferences(item, originalPrefix, newPrefix);
1328
+ return;
1329
+ }
1330
+ const obj = node;
1331
+ if ("string" == typeof obj.canonicalReference && "EntryPoint" !== obj.kind) {
1332
+ const ref = obj.canonicalReference;
1333
+ if (ref.startsWith(originalPrefix)) obj.canonicalReference = ref.replace(originalPrefix, newPrefix);
1334
+ }
1335
+ if (Array.isArray(obj.members)) for (const member of obj.members)rewriteCanonicalReferences(member, originalPrefix, newPrefix);
1336
+ }
1057
1337
  async function ensureTempDeclarationDir(cwd, name) {
1058
1338
  const dir = join(cwd, ".rslib", "declarations", name);
1059
1339
  await rm(dir, {
@@ -1219,7 +1499,26 @@ function runTsgo(options) {
1219
1499
  return !path.includes("__test__/") && !path.includes(".test.d.ts");
1220
1500
  });
1221
1501
  if (0 === dtsFiles.length) return void log.global.warn("No declaration files were generated");
1222
- if (options.bundle) try {
1502
+ if (!options.bundle) {
1503
+ let emittedCount = 0;
1504
+ for (const file of dtsFiles){
1505
+ if (file.relativePath.endsWith(".d.ts.map")) continue;
1506
+ let outputPath = file.relativePath;
1507
+ if (outputPath.startsWith("src/")) outputPath = outputPath.slice(4);
1508
+ if (".d.ts" !== dtsExtension && outputPath.endsWith(".d.ts")) outputPath = outputPath.replace(/\.d\.ts$/, dtsExtension);
1509
+ const jsOutputPath = outputPath.replace(/\.d\.(ts|mts|cts)$/, ".js");
1510
+ if (!context.compilation.assets[jsOutputPath]) continue;
1511
+ let content = await readFile(file.path, "utf-8");
1512
+ content = stripSourceMapComment(content);
1513
+ const source = new context.sources.OriginalSource(content, outputPath);
1514
+ context.compilation.emitAsset(outputPath, source);
1515
+ emittedCount++;
1516
+ if (filesArray && outputPath.endsWith(".d.ts")) filesArray.add(outputPath);
1517
+ }
1518
+ core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted ${emittedCount} declaration file${1 === emittedCount ? "" : "s"} through asset pipeline`);
1519
+ }
1520
+ const apiModelEnabled = true === options.apiModel || "object" == typeof options.apiModel;
1521
+ if (options.bundle || apiModelEnabled) try {
1223
1522
  const exposedPackageJson = api.useExposed("api-extractor-package-json");
1224
1523
  let packageJson;
1225
1524
  if (exposedPackageJson) {
@@ -1230,8 +1529,7 @@ function runTsgo(options) {
1230
1529
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
1231
1530
  packageJson = JSON.parse(packageJsonContent);
1232
1531
  }
1233
- const extractor = new EntryExtractor();
1234
- const { entries } = extractor.extract(packageJson);
1532
+ const { entries, exportPaths } = new EntryExtractor().extract(packageJson);
1235
1533
  const entryPoints = new Map();
1236
1534
  const virtualEntryNames = api.useExposed("virtual-entry-names");
1237
1535
  for (const [entryName, sourcePath] of Object.entries(entries)){
@@ -1260,7 +1558,7 @@ function runTsgo(options) {
1260
1558
  await mkdir(tempBundledDir, {
1261
1559
  recursive: true
1262
1560
  });
1263
- const { bundledFiles, apiModelPath, tsdocMetadataPath, tsdocConfigPath } = await bundleDtsFiles({
1561
+ const { bundledFiles, apiModelPaths, tsdocMetadataPath, tsdocConfigPath } = await bundleDtsFiles({
1264
1562
  cwd,
1265
1563
  tempDtsDir,
1266
1564
  tempOutputDir: tempBundledDir,
@@ -1277,17 +1575,37 @@ function runTsgo(options) {
1277
1575
  apiModel: options.apiModel
1278
1576
  }
1279
1577
  });
1280
- let emittedCount = 0;
1281
- for (const [entryName, tempBundledPath] of bundledFiles){
1282
- const bundledFileName = `${entryName}.d.ts`;
1283
- let content = await readFile(tempBundledPath, "utf-8");
1284
- content = stripSourceMapComment(content);
1285
- const source = new context.sources.OriginalSource(content, bundledFileName);
1286
- context.compilation.emitAsset(bundledFileName, source);
1287
- emittedCount++;
1288
- if (filesArray) filesArray.add(bundledFileName);
1578
+ let apiModelPath;
1579
+ if (1 === apiModelPaths.size) apiModelPath = apiModelPaths.values().next().value;
1580
+ else if (apiModelPaths.size > 1 && packageJson.name) {
1581
+ core_logger.info(`${picocolors.dim(`[${envId}]`)} Merging API models from ${apiModelPaths.size} entries...`);
1582
+ const perEntryModels = new Map();
1583
+ for (const [entryName, modelPath] of apiModelPaths){
1584
+ const content = await readFile(modelPath, "utf-8");
1585
+ perEntryModels.set(entryName, JSON.parse(content));
1586
+ }
1587
+ const mergedModel = mergeApiModels({
1588
+ perEntryModels,
1589
+ packageName: packageJson.name,
1590
+ exportPaths
1591
+ });
1592
+ const mergedPath = join(tempBundledDir, "merged.api.json");
1593
+ await writeFile(mergedPath, JSON.stringify(mergedModel, null, 2), "utf-8");
1594
+ apiModelPath = mergedPath;
1595
+ }
1596
+ if (options.bundle) {
1597
+ let emittedCount = 0;
1598
+ for (const [entryName, tempBundledPath] of bundledFiles){
1599
+ const bundledFileName = `${entryName}.d.ts`;
1600
+ let content = await readFile(tempBundledPath, "utf-8");
1601
+ content = stripSourceMapComment(content);
1602
+ const source = new context.sources.OriginalSource(content, bundledFileName);
1603
+ context.compilation.emitAsset(bundledFileName, source);
1604
+ emittedCount++;
1605
+ if (filesArray) filesArray.add(bundledFileName);
1606
+ }
1607
+ core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted ${emittedCount} bundled declaration file${1 === emittedCount ? "" : "s"} through asset pipeline`);
1289
1608
  }
1290
- core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted ${emittedCount} bundled declaration file${1 === emittedCount ? "" : "s"} through asset pipeline`);
1291
1609
  if (apiModelPath) {
1292
1610
  const defaultApiModelFilename = packageJson.name ? `${getUnscopedPackageName(packageJson.name)}.api.json` : "api.json";
1293
1611
  const apiModelFilename = "object" == typeof options.apiModel && options.apiModel.filename ? options.apiModel.filename : defaultApiModelFilename;
@@ -1297,10 +1615,8 @@ function runTsgo(options) {
1297
1615
  if (filesArray) filesArray.add(`!${apiModelFilename}`);
1298
1616
  core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted API model: ${apiModelFilename} (excluded from npm publish)`);
1299
1617
  const localPaths = "object" == typeof options.apiModel ? options.apiModel.localPaths : void 0;
1300
- const isCI = "true" === process.env.GITHUB_ACTIONS || "true" === process.env.CI;
1301
- if (localPaths && localPaths.length > 0 && !isCI) {
1302
- const tsdocMetadataOption = "object" == typeof options.apiModel ? options.apiModel.tsdocMetadata : void 0;
1303
- const localTsdocFilename = "object" == typeof tsdocMetadataOption && tsdocMetadataOption.filename ? tsdocMetadataOption.filename : "tsdoc-metadata.json";
1618
+ if (localPaths && localPaths.length > 0 && !TsDocConfigBuilder.isCI()) {
1619
+ const localTsdocFilename = resolveTsdocMetadataFilename(options.apiModel);
1304
1620
  api.expose("dts-local-paths-data", {
1305
1621
  localPaths,
1306
1622
  apiModelFilename,
@@ -1313,8 +1629,7 @@ function runTsgo(options) {
1313
1629
  }
1314
1630
  }
1315
1631
  if (tsdocMetadataPath) {
1316
- const tsdocMetadataOption = "object" == typeof options.apiModel ? options.apiModel.tsdocMetadata : void 0;
1317
- const tsdocMetadataFilename = "object" == typeof tsdocMetadataOption && tsdocMetadataOption.filename ? tsdocMetadataOption.filename : "tsdoc-metadata.json";
1632
+ const tsdocMetadataFilename = resolveTsdocMetadataFilename(options.apiModel);
1318
1633
  const tsdocMetadataContent = (await readFile(tsdocMetadataPath, "utf-8")).replaceAll("\r\n", "\n");
1319
1634
  const tsdocMetadataSource = new context.sources.OriginalSource(tsdocMetadataContent, tsdocMetadataFilename);
1320
1635
  context.compilation.emitAsset(tsdocMetadataFilename, tsdocMetadataSource);
@@ -1337,7 +1652,7 @@ function runTsgo(options) {
1337
1652
  if (filesArray) filesArray.add("!tsconfig.json");
1338
1653
  core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted resolved tsconfig: tsconfig.json (excluded from npm publish)`);
1339
1654
  }
1340
- for (const [entryName] of bundledFiles){
1655
+ if (options.bundle) for (const [entryName] of bundledFiles){
1341
1656
  const bundledFileName = `${entryName}.d.ts`;
1342
1657
  const mapFileName = `${bundledFileName}.map`;
1343
1658
  for (const file of dtsFiles){
@@ -1355,22 +1670,6 @@ function runTsgo(options) {
1355
1670
  log.global.error("Failed to bundle declaration files:", error);
1356
1671
  if (abortOnError) throw error;
1357
1672
  }
1358
- else {
1359
- let emittedCount = 0;
1360
- for (const file of dtsFiles){
1361
- if (file.relativePath.endsWith(".d.ts.map")) continue;
1362
- let content = await readFile(file.path, "utf-8");
1363
- let outputPath = file.relativePath;
1364
- if (outputPath.startsWith("src/")) outputPath = outputPath.slice(4);
1365
- if (".d.ts" !== dtsExtension && outputPath.endsWith(".d.ts")) outputPath = outputPath.replace(/\.d\.ts$/, dtsExtension);
1366
- content = stripSourceMapComment(content);
1367
- const source = new context.sources.OriginalSource(content, outputPath);
1368
- context.compilation.emitAsset(outputPath, source);
1369
- emittedCount++;
1370
- if (filesArray && outputPath.endsWith(".d.ts")) filesArray.add(outputPath);
1371
- }
1372
- core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted ${emittedCount} declaration file${1 === emittedCount ? "" : "s"} through asset pipeline`);
1373
- }
1374
1673
  } catch (error) {
1375
1674
  log.global.error("Failed to generate declaration files:", error);
1376
1675
  if (abortOnError) throw error;
@@ -1969,223 +2268,6 @@ const PackageJsonTransformPlugin = (options = {})=>({
1969
2268
  });
1970
2269
  }
1971
2270
  });
1972
- class ImportGraph {
1973
- options;
1974
- sys;
1975
- program = null;
1976
- compilerOptions = null;
1977
- moduleResolutionCache = null;
1978
- constructor(options){
1979
- this.options = options;
1980
- this.sys = options.sys ?? typescript.sys;
1981
- }
1982
- traceFromEntries(entryPaths) {
1983
- const errors = [];
1984
- const visited = new Set();
1985
- const entries = [];
1986
- const initResult = this.initializeProgram();
1987
- if (!initResult.success) return {
1988
- files: [],
1989
- entries: [],
1990
- errors: [
1991
- initResult.error
1992
- ]
1993
- };
1994
- for (const entryPath of entryPaths){
1995
- const absolutePath = this.resolveEntryPath(entryPath);
1996
- if (!this.sys.fileExists(absolutePath)) {
1997
- errors.push({
1998
- type: "entry_not_found",
1999
- message: `Entry file not found: ${entryPath}`,
2000
- path: absolutePath
2001
- });
2002
- continue;
2003
- }
2004
- entries.push(absolutePath);
2005
- this.traceImports(absolutePath, visited, errors);
2006
- }
2007
- const files = Array.from(visited).filter((file)=>this.isSourceFile(file));
2008
- return {
2009
- files: files.sort(),
2010
- entries,
2011
- errors
2012
- };
2013
- }
2014
- traceFromPackageExports(packageJsonPath) {
2015
- const absolutePath = this.resolveEntryPath(packageJsonPath);
2016
- let packageJson;
2017
- try {
2018
- const content = this.sys.readFile(absolutePath);
2019
- if (!content) return {
2020
- files: [],
2021
- entries: [],
2022
- errors: [
2023
- {
2024
- type: "package_json_not_found",
2025
- message: `Failed to read package.json: File not found at ${absolutePath}`,
2026
- path: absolutePath
2027
- }
2028
- ]
2029
- };
2030
- packageJson = JSON.parse(content);
2031
- } catch (error) {
2032
- const message = error instanceof Error ? error.message : String(error);
2033
- return {
2034
- files: [],
2035
- entries: [],
2036
- errors: [
2037
- {
2038
- type: "package_json_parse_error",
2039
- message: `Failed to parse package.json: ${message}`,
2040
- path: absolutePath
2041
- }
2042
- ]
2043
- };
2044
- }
2045
- const extractor = new EntryExtractor();
2046
- const { entries } = extractor.extract(packageJson);
2047
- const packageDir = dirname(absolutePath);
2048
- const entryPaths = Object.values(entries).map((p)=>external_node_path_resolve(packageDir, p));
2049
- return this.traceFromEntries(entryPaths);
2050
- }
2051
- initializeProgram() {
2052
- if (this.program) return {
2053
- success: true
2054
- };
2055
- const configPath = this.findTsConfig();
2056
- if (!configPath) return {
2057
- success: false,
2058
- error: {
2059
- type: "tsconfig_not_found",
2060
- message: `No tsconfig.json found in ${this.options.rootDir}`,
2061
- path: this.options.rootDir
2062
- }
2063
- };
2064
- const configFile = typescript.readConfigFile(configPath, (path)=>this.sys.readFile(path));
2065
- if (configFile.error) {
2066
- const message = typescript.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
2067
- return {
2068
- success: false,
2069
- error: {
2070
- type: "tsconfig_read_error",
2071
- message: `Failed to read tsconfig.json: ${message}`,
2072
- path: configPath
2073
- }
2074
- };
2075
- }
2076
- const parsed = typescript.parseJsonConfigFileContent(configFile.config, this.sys, dirname(configPath));
2077
- if (parsed.errors.length > 0) {
2078
- const messages = parsed.errors.map((e)=>typescript.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
2079
- return {
2080
- success: false,
2081
- error: {
2082
- type: "tsconfig_parse_error",
2083
- message: `Failed to parse tsconfig.json: ${messages}`,
2084
- path: configPath
2085
- }
2086
- };
2087
- }
2088
- this.compilerOptions = parsed.options;
2089
- this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
2090
- const host = typescript.createCompilerHost(this.compilerOptions, true);
2091
- host.getCurrentDirectory = ()=>this.options.rootDir;
2092
- this.program = typescript.createProgram([], this.compilerOptions, host);
2093
- return {
2094
- success: true
2095
- };
2096
- }
2097
- findTsConfig() {
2098
- if (this.options.tsconfigPath) {
2099
- const customPath = isAbsolute(this.options.tsconfigPath) ? this.options.tsconfigPath : external_node_path_resolve(this.options.rootDir, this.options.tsconfigPath);
2100
- if (this.sys.fileExists(customPath)) return customPath;
2101
- return null;
2102
- }
2103
- const configPath = typescript.findConfigFile(this.options.rootDir, (path)=>this.sys.fileExists(path));
2104
- return configPath ?? null;
2105
- }
2106
- resolveEntryPath(entryPath) {
2107
- if (isAbsolute(entryPath)) return normalize(entryPath);
2108
- return normalize(external_node_path_resolve(this.options.rootDir, entryPath));
2109
- }
2110
- traceImports(filePath, visited, errors) {
2111
- const normalizedPath = normalize(filePath);
2112
- if (visited.has(normalizedPath)) return;
2113
- if (this.isExternalModule(normalizedPath)) return;
2114
- visited.add(normalizedPath);
2115
- const content = this.sys.readFile(normalizedPath);
2116
- if (!content) return void errors.push({
2117
- type: "file_read_error",
2118
- message: `Failed to read file: ${normalizedPath}`,
2119
- path: normalizedPath
2120
- });
2121
- const sourceFile = typescript.createSourceFile(normalizedPath, content, typescript.ScriptTarget.Latest, true);
2122
- const imports = this.extractImports(sourceFile);
2123
- for (const importPath of imports){
2124
- const resolved = this.resolveImport(importPath, normalizedPath);
2125
- if (resolved) this.traceImports(resolved, visited, errors);
2126
- }
2127
- }
2128
- extractImports(sourceFile) {
2129
- const imports = [];
2130
- const visit = (node)=>{
2131
- if (typescript.isImportDeclaration(node)) {
2132
- const specifier = node.moduleSpecifier;
2133
- if (typescript.isStringLiteral(specifier)) imports.push(specifier.text);
2134
- } else if (typescript.isExportDeclaration(node)) {
2135
- const specifier = node.moduleSpecifier;
2136
- if (specifier && typescript.isStringLiteral(specifier)) imports.push(specifier.text);
2137
- } else if (typescript.isCallExpression(node)) {
2138
- const expression = node.expression;
2139
- if (expression.kind === typescript.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
2140
- const arg = node.arguments[0];
2141
- if (arg && typescript.isStringLiteral(arg)) imports.push(arg.text);
2142
- }
2143
- }
2144
- typescript.forEachChild(node, visit);
2145
- };
2146
- visit(sourceFile);
2147
- return imports;
2148
- }
2149
- resolveImport(specifier, fromFile) {
2150
- if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
2151
- if (!this.compilerOptions?.paths || !Object.keys(this.compilerOptions.paths).length) return null;
2152
- }
2153
- if (!this.compilerOptions || !this.moduleResolutionCache) return null;
2154
- const resolved = typescript.resolveModuleName(specifier, fromFile, this.compilerOptions, this.sys, this.moduleResolutionCache);
2155
- if (resolved.resolvedModule) {
2156
- const resolvedPath = resolved.resolvedModule.resolvedFileName;
2157
- if (resolved.resolvedModule.isExternalLibraryImport) return null;
2158
- if (resolvedPath.endsWith(".d.ts")) {
2159
- const sourcePath = resolvedPath.replace(/\.d\.ts$/, ".ts");
2160
- if (this.sys.fileExists(sourcePath)) return sourcePath;
2161
- return null;
2162
- }
2163
- return resolvedPath;
2164
- }
2165
- return null;
2166
- }
2167
- isExternalModule(filePath) {
2168
- return filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\");
2169
- }
2170
- isSourceFile(filePath) {
2171
- if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) return false;
2172
- if (filePath.endsWith(".d.ts")) return false;
2173
- if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
2174
- if (filePath.includes("/__test__/") || filePath.includes("\\__test__\\")) return false;
2175
- if (filePath.includes("/__tests__/") || filePath.includes("\\__tests__\\")) return false;
2176
- const excludePatterns = this.options.excludePatterns ?? [];
2177
- for (const pattern of excludePatterns)if (filePath.includes(pattern)) return false;
2178
- return true;
2179
- }
2180
- static fromEntries(entryPaths, options) {
2181
- const graph = new ImportGraph(options);
2182
- return graph.traceFromEntries(entryPaths);
2183
- }
2184
- static fromPackageExports(packageJsonPath, options) {
2185
- const graph = new ImportGraph(options);
2186
- return graph.traceFromPackageExports(packageJsonPath);
2187
- }
2188
- }
2189
2271
  function formatLintResults(results, cwd) {
2190
2272
  if (0 === results.messages.length) return "";
2191
2273
  const lines = [];
@@ -2229,6 +2311,41 @@ function discoverFilesToLint(options, cwd) {
2229
2311
  isGlobPattern: false
2230
2312
  };
2231
2313
  }
2314
+ function discoverFilesToLintPerEntry(cwd) {
2315
+ const packageJsonPath = join(cwd, "package.json");
2316
+ let packageJson;
2317
+ try {
2318
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2319
+ } catch {
2320
+ return {
2321
+ perEntry: new Map(),
2322
+ errors: []
2323
+ };
2324
+ }
2325
+ const { entries } = new EntryExtractor().extract(packageJson);
2326
+ const graph = new ImportGraph({
2327
+ rootDir: cwd
2328
+ });
2329
+ const perEntry = new Map();
2330
+ const allErrors = [];
2331
+ for (const [entryName, sourcePath] of Object.entries(entries)){
2332
+ if (entryName.startsWith("bin/")) continue;
2333
+ const resolvedPath = sourcePath.startsWith(".") ? external_node_path_resolve(cwd, sourcePath) : sourcePath;
2334
+ const result = graph.traceFromEntries([
2335
+ resolvedPath
2336
+ ]);
2337
+ perEntry.set(entryName, {
2338
+ files: result.files,
2339
+ errors: result.errors,
2340
+ isGlobPattern: false
2341
+ });
2342
+ allErrors.push(...result.errors);
2343
+ }
2344
+ return {
2345
+ perEntry,
2346
+ errors: allErrors
2347
+ };
2348
+ }
2232
2349
  async function runTsDocLint(options, cwd) {
2233
2350
  const tsdocOptions = options.tsdoc ?? {};
2234
2351
  const persistConfig = options.persistConfig;
@@ -2345,6 +2462,112 @@ async function runTsDocLint(options, cwd) {
2345
2462
  }
2346
2463
  };
2347
2464
  }
2465
+ async function runTsDocLintPerEntry(options, cwd) {
2466
+ const tsdocOptions = options.tsdoc ?? {};
2467
+ const persistConfig = options.persistConfig;
2468
+ const shouldPersist = TsDocConfigBuilder.shouldPersist(persistConfig);
2469
+ const tsdocConfigOutputPath = TsDocConfigBuilder.getConfigPath(persistConfig, cwd);
2470
+ const skipCIValidation = false === persistConfig;
2471
+ const tsdocConfigPath = await TsDocConfigBuilder.writeConfigFile(tsdocOptions, dirname(tsdocConfigOutputPath), skipCIValidation);
2472
+ const eslintModule = await import("eslint");
2473
+ const tsParserModule = await import("@typescript-eslint/parser");
2474
+ const tsdocPluginModule = await import("eslint-plugin-tsdoc");
2475
+ const { ESLint } = eslintModule;
2476
+ const tsParser = tsParserModule.default ?? tsParserModule;
2477
+ const tsdocPlugin = tsdocPluginModule.default ?? tsdocPluginModule;
2478
+ const { perEntry, errors: allErrors } = discoverFilesToLintPerEntry(cwd);
2479
+ if (0 === perEntry.size) return {
2480
+ results: {
2481
+ errorCount: 0,
2482
+ warningCount: 0,
2483
+ messages: []
2484
+ },
2485
+ ...shouldPersist && {
2486
+ tsdocConfigPath
2487
+ },
2488
+ ...allErrors.length > 0 && {
2489
+ discoveryErrors: allErrors
2490
+ }
2491
+ };
2492
+ const eslint = new ESLint({
2493
+ cwd,
2494
+ overrideConfigFile: true,
2495
+ overrideConfig: [
2496
+ {
2497
+ ignores: [
2498
+ "**/node_modules/**",
2499
+ "**/dist/**",
2500
+ "**/coverage/**"
2501
+ ]
2502
+ },
2503
+ {
2504
+ files: [
2505
+ "**/*.ts",
2506
+ "**/*.tsx"
2507
+ ],
2508
+ languageOptions: {
2509
+ parser: tsParser
2510
+ },
2511
+ plugins: {
2512
+ tsdoc: tsdocPlugin
2513
+ },
2514
+ rules: {
2515
+ "tsdoc/syntax": "error"
2516
+ }
2517
+ }
2518
+ ]
2519
+ });
2520
+ const allMessages = [];
2521
+ let totalErrors = 0;
2522
+ let totalWarnings = 0;
2523
+ for (const [entryName, discovery] of perEntry){
2524
+ if (0 === discovery.files.length) continue;
2525
+ const exportKey = "index" === entryName ? "." : `./${entryName}`;
2526
+ core_logger.info(`${picocolors.dim("[tsdoc-lint]")} Validating "${exportKey}" (${discovery.files.length} file${1 === discovery.files.length ? "" : "s"})...`);
2527
+ const eslintResults = await eslint.lintFiles(discovery.files);
2528
+ let entryErrors = 0;
2529
+ let entryWarnings = 0;
2530
+ for (const result of eslintResults)for (const msg of result.messages){
2531
+ allMessages.push({
2532
+ filePath: result.filePath,
2533
+ line: msg.line,
2534
+ column: msg.column,
2535
+ message: msg.message,
2536
+ ruleId: msg.ruleId,
2537
+ severity: msg.severity
2538
+ });
2539
+ if (2 === msg.severity) {
2540
+ entryErrors++;
2541
+ totalErrors++;
2542
+ } else {
2543
+ entryWarnings++;
2544
+ totalWarnings++;
2545
+ }
2546
+ }
2547
+ if (0 === entryErrors && 0 === entryWarnings) core_logger.info(`${picocolors.dim("[tsdoc-lint]")} ${picocolors.green(`\u2713 "${exportKey}" - All TSDoc comments valid`)}`);
2548
+ else {
2549
+ const errorText = 1 === entryErrors ? "error" : "errors";
2550
+ const warningText = 1 === entryWarnings ? "warning" : "warnings";
2551
+ const parts = [];
2552
+ if (entryErrors > 0) parts.push(`${entryErrors} ${errorText}`);
2553
+ if (entryWarnings > 0) parts.push(`${entryWarnings} ${warningText}`);
2554
+ core_logger.warn(`${picocolors.dim("[tsdoc-lint]")} ${picocolors.red(`\u2717 "${exportKey}" - ${parts.join(", ")} found`)}`);
2555
+ }
2556
+ }
2557
+ return {
2558
+ results: {
2559
+ errorCount: totalErrors,
2560
+ warningCount: totalWarnings,
2561
+ messages: allMessages
2562
+ },
2563
+ ...shouldPersist && {
2564
+ tsdocConfigPath
2565
+ },
2566
+ ...allErrors.length > 0 && {
2567
+ discoveryErrors: allErrors
2568
+ }
2569
+ };
2570
+ }
2348
2571
  async function cleanupTsDocConfig(configPath) {
2349
2572
  if (!configPath) return;
2350
2573
  try {
@@ -2365,7 +2588,7 @@ const TsDocLintPlugin = (options = {})=>{
2365
2588
  const onError = options.onError ?? (isCI ? "throw" : "error");
2366
2589
  core_logger.info(`${picocolors.dim("[tsdoc-lint]")} Validating TSDoc comments...`);
2367
2590
  try {
2368
- const { results, tsdocConfigPath, discoveryErrors } = await runTsDocLint(options, cwd);
2591
+ const { results, tsdocConfigPath, discoveryErrors } = options.perEntry ? await runTsDocLintPerEntry(options, cwd) : await runTsDocLint(options, cwd);
2369
2592
  if (discoveryErrors && discoveryErrors.length > 0) for (const error of discoveryErrors)core_logger.warn(`${picocolors.dim("[tsdoc-lint]")} ${error.message}`);
2370
2593
  if (!TsDocConfigBuilder.shouldPersist(options.persistConfig)) tempTsDocConfigPath = tsdocConfigPath;
2371
2594
  if (0 === results.errorCount && 0 === results.warningCount) return void core_logger.info(`${picocolors.dim("[tsdoc-lint]")} ${picocolors.green("All TSDoc comments are valid")}`);
@@ -2422,7 +2645,8 @@ const VirtualEntryPlugin = (options)=>{
2422
2645
  "npm"
2423
2646
  ],
2424
2647
  externals: [],
2425
- apiModel: true
2648
+ apiModel: true,
2649
+ bundle: true
2426
2650
  };
2427
2651
  static mergeOptions(options = {}) {
2428
2652
  const copyPatterns = [
@@ -2442,6 +2666,7 @@ const VirtualEntryPlugin = (options)=>{
2442
2666
  targets: options.targets ?? NodeLibraryBuilder.DEFAULT_OPTIONS.targets,
2443
2667
  externals: options.externals ?? NodeLibraryBuilder.DEFAULT_OPTIONS.externals,
2444
2668
  apiModel: options.apiModel ?? NodeLibraryBuilder.DEFAULT_OPTIONS.apiModel,
2669
+ bundle: options.bundle ?? NodeLibraryBuilder.DEFAULT_OPTIONS.bundle,
2445
2670
  ...void 0 !== options.entry && {
2446
2671
  entry: options.entry
2447
2672
  },
@@ -2487,20 +2712,31 @@ const VirtualEntryPlugin = (options)=>{
2487
2712
  },
2488
2713
  ..."object" == typeof lintConfig ? lintConfig : {}
2489
2714
  };
2490
- plugins.push(TsDocLintPlugin(lintOptions));
2715
+ plugins.push(TsDocLintPlugin({
2716
+ ...lintOptions,
2717
+ ...false === options.bundle && {
2718
+ perEntry: true
2719
+ }
2720
+ }));
2491
2721
  }
2492
- if (!options.entry) plugins.push(AutoEntryPlugin(null != options.exportsAsIndexes ? {
2493
- exportsAsIndexes: options.exportsAsIndexes
2494
- } : void 0));
2722
+ if (!options.entry) plugins.push(AutoEntryPlugin({
2723
+ ...null != options.exportsAsIndexes && {
2724
+ exportsAsIndexes: options.exportsAsIndexes
2725
+ },
2726
+ ...false === options.bundle && {
2727
+ bundleless: true
2728
+ }
2729
+ }));
2495
2730
  const userTransform = options.transform;
2496
2731
  const transformFn = userTransform ? (pkg)=>userTransform({
2497
2732
  target,
2498
2733
  pkg
2499
2734
  }) : void 0;
2500
2735
  const libraryFormat = options.format ?? "esm";
2736
+ const collapseIndex = (options.bundle ?? true) || !(options.exportsAsIndexes ?? false);
2501
2737
  plugins.push(PackageJsonTransformPlugin({
2502
2738
  forcePrivate: "dev" === target,
2503
- bundle: true,
2739
+ bundle: collapseIndex,
2504
2740
  target,
2505
2741
  format: libraryFormat,
2506
2742
  ...transformFn && {
@@ -2513,16 +2749,32 @@ const VirtualEntryPlugin = (options)=>{
2513
2749
  transformFiles: options.transformFiles
2514
2750
  }
2515
2751
  }));
2516
- if (options.plugins) plugins.push(...options.plugins);
2752
+ plugins.push(...options.plugins);
2517
2753
  const outputDir = `dist/${target}`;
2518
- const entry = options.entry;
2754
+ let entry = options.entry;
2755
+ if (false === options.bundle && !entry) {
2756
+ const cwd = process.cwd();
2757
+ const packageJsonPath = join(cwd, "package.json");
2758
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2759
+ const { entries } = new EntryExtractor().extract(packageJson);
2760
+ const graph = new ImportGraph({
2761
+ rootDir: cwd
2762
+ });
2763
+ const result = graph.traceFromEntries(Object.values(entries));
2764
+ const tracedEntries = {};
2765
+ for (const file of result.files){
2766
+ const relPath = relative(cwd, file);
2767
+ tracedEntries[relPath] = `./${relPath}`;
2768
+ }
2769
+ entry = tracedEntries;
2770
+ }
2519
2771
  const apiModelForTarget = "npm" === target ? options.apiModel : void 0;
2520
2772
  plugins.push(DtsPlugin({
2521
2773
  ...options.tsconfigPath && {
2522
2774
  tsconfigPath: options.tsconfigPath
2523
2775
  },
2524
2776
  abortOnError: true,
2525
- bundle: true,
2777
+ bundle: options.bundle ?? true,
2526
2778
  ...options.dtsBundledPackages && {
2527
2779
  bundledPackages: options.dtsBundledPackages
2528
2780
  },
@@ -2534,12 +2786,16 @@ const VirtualEntryPlugin = (options)=>{
2534
2786
  }));
2535
2787
  const lib = {
2536
2788
  id: target,
2537
- outBase: outputDir,
2789
+ outBase: false === options.bundle ? "src" : outputDir,
2538
2790
  output: {
2539
2791
  target: "node",
2540
2792
  module: true,
2541
2793
  cleanDistPath: true,
2542
2794
  sourceMap: "dev" === target,
2795
+ // Prevent @preserve comments from generating .LICENSE.txt files
2796
+ ...false === options.bundle && {
2797
+ legalComments: "inline"
2798
+ },
2543
2799
  distPath: {
2544
2800
  root: outputDir
2545
2801
  },
@@ -2554,7 +2810,7 @@ const VirtualEntryPlugin = (options)=>{
2554
2810
  experiments: {
2555
2811
  advancedEsm: "esm" === libraryFormat
2556
2812
  },
2557
- bundle: true,
2813
+ bundle: options.bundle ?? true,
2558
2814
  plugins,
2559
2815
  source: {
2560
2816
  ...options.tsconfigPath && {
@@ -2579,10 +2835,13 @@ const VirtualEntryPlugin = (options)=>{
2579
2835
  const virtualByFormat = new Map();
2580
2836
  for (const [outputName, config] of Object.entries(virtualEntries)){
2581
2837
  const entryFormat = config.format ?? libraryFormat;
2582
- if (!virtualByFormat.has(entryFormat)) virtualByFormat.set(entryFormat, new Map());
2838
+ let formatMap = virtualByFormat.get(entryFormat);
2839
+ if (!formatMap) {
2840
+ formatMap = new Map();
2841
+ virtualByFormat.set(entryFormat, formatMap);
2842
+ }
2583
2843
  const entryName = outputName.replace(/\.(c|m)?js$/, "");
2584
- const formatMap = virtualByFormat.get(entryFormat);
2585
- if (formatMap) formatMap.set(entryName, config.source);
2844
+ formatMap.set(entryName, config.source);
2586
2845
  }
2587
2846
  for (const [format, entries] of virtualByFormat){
2588
2847
  const virtualEntryNames = new Set(entries.keys());
@@ -2656,4 +2915,4 @@ const VirtualEntryPlugin = (options)=>{
2656
2915
  }
2657
2916
  }
2658
2917
  }
2659
- export { AutoEntryPlugin, DtsPlugin, EntryExtractor, FilesArrayPlugin, ImportGraph, NodeLibraryBuilder, PackageJsonTransformPlugin, TsDocConfigBuilder, TsDocLintPlugin, TsconfigResolver, TsconfigResolverError, VirtualEntryPlugin, extractEntriesFromPackageJson };
2918
+ export { AutoEntryPlugin, DtsPlugin, EntryExtractor, FilesArrayPlugin, ImportGraph, NodeLibraryBuilder, PackageJsonTransformPlugin, TsDocConfigBuilder, TsDocLintPlugin, TsconfigResolver, TsconfigResolverError, VirtualEntryPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/rslib-builder",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
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
  "keywords": [
@@ -47,7 +47,7 @@
47
47
  "@microsoft/tsdoc-config": "^0.18.0",
48
48
  "@pnpm/catalogs.config": "^1000.0.5",
49
49
  "@pnpm/catalogs.protocol-parser": "^1001.0.0",
50
- "@pnpm/exportable-manifest": "^1000.3.1",
50
+ "@pnpm/exportable-manifest": "^1000.4.0",
51
51
  "@pnpm/lockfile.fs": "^1001.1.29",
52
52
  "@pnpm/workspace.read-manifest": "^1000.2.10",
53
53
  "@typescript-eslint/parser": "^8.53.1",
@@ -57,14 +57,14 @@
57
57
  "glob": "^13.0.1",
58
58
  "picocolors": "^1.1.1",
59
59
  "sort-package-json": "^3.6.1",
60
- "tmp": "^0.2.5",
60
+ "tmp": ">=0.2.4",
61
61
  "workspace-tools": "^0.41.0"
62
62
  },
63
63
  "peerDependencies": {
64
- "@microsoft/api-extractor": "^7.55.2",
65
- "@rslib/core": "^0.19.3",
66
- "@types/node": "^25.0.10",
67
- "@typescript/native-preview": "^7.0.0-dev.20260124.1",
64
+ "@microsoft/api-extractor": "^7.56.3",
65
+ "@rslib/core": "^0.19.5",
66
+ "@types/node": "^25.2.1",
67
+ "@typescript/native-preview": "7.0.0-dev.20260207.1",
68
68
  "typescript": "^5.9.3"
69
69
  },
70
70
  "peerDependenciesMeta": {
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.56.0"
8
+ "packageVersion": "7.56.3"
9
9
  }
10
10
  ]
11
11
  }