@savvy-web/rslib-builder 0.12.2 → 0.13.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.
Files changed (4) hide show
  1. package/README.md +13 -16
  2. package/index.d.ts +63 -2
  3. package/index.js +299 -88
  4. package/package.json +4 -4
package/README.md CHANGED
@@ -4,29 +4,26 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D24.0.0-brightgreen)](https://nodejs.org)
6
6
 
7
- Build modern ESM Node.js libraries with minimal configuration. Handles
8
- TypeScript declarations, package.json transformations, and PNPM workspace
9
- resolution automatically.
7
+ Build modern ESM Node.js libraries with minimal configuration. Handles TypeScript declarations, package.json transformations, and PNPM workspace resolution automatically.
10
8
 
11
9
  ## Features
12
10
 
13
- - **Zero-Config Entry Detection** - Auto-discovers entry points from package.json
14
- exports, no manual configuration needed
15
- - **10-100x Faster Types** - Uses tsgo (native TypeScript compiler) with API
16
- Extractor for bundled, clean public API declarations
17
- - **Production-Ready Transforms** - Converts `.ts` exports to `.js`, resolves
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
21
- - **Multi-Target Builds** - Separate dev (with source maps) and npm (optimized)
22
- outputs from a single configuration
23
- - **TSDoc Validation** - Pre-build documentation validation with automatic
24
- public API discovery
11
+ - **Zero-Config Entry Detection** - Auto-discovers entry points from package.json exports
12
+ - **10-100x Faster Types** - Uses tsgo (native TypeScript compiler) with API Extractor for bundled, clean public API declarations
13
+ - **Production-Ready Transforms** - Converts `.ts` exports to `.js`, resolves PNPM `catalog:` and `workspace:` references, generates files array
14
+ - **Bundled or Bundleless** - Choose single-file bundles per entry or bundleless mode that preserves your source file structure
15
+ - **Multi-Target Builds** - Separate dev (with source maps) and npm (optimized) outputs from a single configuration
16
+ - **Flexible Formats** - ESM, CJS, or dual format output with per-entry format overrides
17
+ - **TSDoc Validation** - Pre-build documentation validation with automatic public API discovery
25
18
 
26
19
  ## Installation
27
20
 
28
21
  ```bash
29
- npm install --save-dev @savvy-web/rslib-builder @rslib/core @microsoft/api-extractor @typescript/native-preview
22
+ npm install --save-dev \
23
+ @savvy-web/rslib-builder \
24
+ @rslib/core \
25
+ @microsoft/api-extractor \
26
+ @typescript/native-preview
30
27
  ```
31
28
 
32
29
  ## Quick Start
package/index.d.ts CHANGED
@@ -421,6 +421,13 @@ export declare interface DtsPluginOptions {
421
421
  * The API model is excluded from npm publish (not added to `files` array).
422
422
  */
423
423
  apiModel?: ApiModelOptions | boolean;
424
+ /**
425
+ * Path prefix for emitted DTS files.
426
+ * Used in dual format builds to place declarations in format subdirectories
427
+ * (e.g., `esm/index.d.ts`, `cjs/index.d.cts`).
428
+ * Metadata files (api.json, tsdoc-metadata.json, tsconfig.json) are NOT prefixed.
429
+ */
430
+ dtsPathPrefix?: string;
424
431
  }
425
432
 
426
433
  /**
@@ -647,6 +654,14 @@ export declare interface FilesArrayPluginOptions<TTarget extends string = string
647
654
  * Passed to the `transformFiles` callback to allow target-specific transformations.
648
655
  */
649
656
  target: TTarget;
657
+ /**
658
+ * Format directories to include in the files array.
659
+ * Used in dual format builds so npm's `files` field includes
660
+ * format directories (e.g., `["esm", "cjs"]`).
661
+ * Directory names are treated as recursive includes by npm,
662
+ * so individual files under these dirs are filtered out.
663
+ */
664
+ formatDirs?: string[];
650
665
  }
651
666
 
652
667
  /**
@@ -1238,9 +1253,42 @@ export declare interface NodeLibraryBuilderOptions {
1238
1253
  * - `"esm"` → `"type": "module"`
1239
1254
  * - `"cjs"` → `"type": "commonjs"`
1240
1255
  *
1256
+ * When an array is provided, the package is built in both formats.
1257
+ * The first format in the array is the primary format (determines `type` field).
1258
+ * Each format outputs to its own subdirectory (`dist/{target}/esm/`, `dist/{target}/cjs/`).
1259
+ *
1241
1260
  * @defaultValue `"esm"`
1261
+ *
1262
+ * @example
1263
+ * Dual format output:
1264
+ * ```typescript
1265
+ * NodeLibraryBuilder.create({
1266
+ * format: ['esm', 'cjs'],
1267
+ * })
1268
+ * ```
1242
1269
  */
1243
- format?: LibraryFormat;
1270
+ format?: LibraryFormat | LibraryFormat[];
1271
+ /**
1272
+ * Per-entry format overrides.
1273
+ * Maps export paths (matching package.json exports keys like `"./markdownlint"`)
1274
+ * to a specific format. Entries not listed inherit the top-level `format`.
1275
+ *
1276
+ * @remarks
1277
+ * When both `entryFormats` and array `format` are used, `entryFormats` takes precedence.
1278
+ * An entry with a specific format override will only be built in that format,
1279
+ * even if the global format is dual.
1280
+ *
1281
+ * @example
1282
+ * ```typescript
1283
+ * NodeLibraryBuilder.create({
1284
+ * format: 'esm',
1285
+ * entryFormats: {
1286
+ * './markdownlint': 'cjs',
1287
+ * },
1288
+ * })
1289
+ * ```
1290
+ */
1291
+ entryFormats?: Record<string, LibraryFormat>;
1244
1292
  /**
1245
1293
  * Additional entry points bundled with custom output names.
1246
1294
  * These entries bypass type generation and package.json exports
@@ -2081,6 +2129,18 @@ export declare interface PackageJsonTransformPluginOptions {
2081
2129
  * ```
2082
2130
  */
2083
2131
  transform?: (pkg: PackageJson) => PackageJson;
2132
+ /**
2133
+ * Per-entry format overrides for export conditions.
2134
+ * Maps export paths to their format (e.g., `{ "./markdownlint": "cjs" }`).
2135
+ * Entries not listed inherit the top-level `format`.
2136
+ */
2137
+ entryFormats?: Record<string, LibraryFormat>;
2138
+ /**
2139
+ * Whether the build uses dual format (both ESM and CJS).
2140
+ * When true, exports get both `import` and `require` conditions
2141
+ * with format directory prefixes.
2142
+ */
2143
+ dualFormat?: boolean;
2084
2144
  }
2085
2145
 
2086
2146
  /**
@@ -3124,7 +3184,8 @@ export declare class TsconfigResolver {
3124
3184
  source: string;
3125
3185
  /**
3126
3186
  * Output format for this entry.
3127
- * If not specified, inherits from top-level `format` option.
3187
+ * If not specified, inherits from the primary format
3188
+ * (first element when `format` is an array, or the single format value).
3128
3189
  */
3129
3190
  format?: LibraryFormat;
3130
3191
  }
package/index.js CHANGED
@@ -465,28 +465,24 @@ const AutoEntryPlugin = (options)=>{
465
465
  tracedEntries[relPath] = `./${relPath}`;
466
466
  }
467
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])=>{
475
- lib.source = {
468
+ for (const [, lib] of environments)lib.source = {
476
469
  ...lib.source,
477
- entry: {
478
- ...lib.source?.entry,
479
- ...entries
480
- }
470
+ entry: tracedEntries
481
471
  };
482
- });
472
+ } else for (const [, lib] of environments)lib.source = {
473
+ ...lib.source,
474
+ entry: {
475
+ ...lib.source?.entry,
476
+ ...entries
477
+ }
478
+ };
483
479
  const state = buildStateMap.get(api);
484
480
  if (state && !state.hasLoggedEntries) {
485
481
  state.hasLoggedEntries = true;
486
- environments.forEach(([env])=>{
482
+ for (const [env] of environments){
487
483
  const log = createEnvLogger(env);
488
484
  log.entries("auto-detected entries", entries);
489
- });
485
+ }
490
486
  }
491
487
  }
492
488
  } catch (error) {
@@ -977,8 +973,7 @@ class TsDocConfigBuilder {
977
973
  }));
978
974
  }
979
975
  static shouldPersist(persistConfig) {
980
- if (false === persistConfig) return false;
981
- return true;
976
+ return false !== persistConfig;
982
977
  }
983
978
  static getConfigPath(persistConfig, cwd) {
984
979
  if ("string" == typeof persistConfig) return isAbsolute(persistConfig) ? persistConfig : join(cwd, persistConfig);
@@ -1156,7 +1151,8 @@ async function bundleDtsFiles(options) {
1156
1151
  } catch {}
1157
1152
  }
1158
1153
  }
1159
- const outputFileName = `${entryName}.d.ts`;
1154
+ const dtsExtension = "cjs" === options.format ? ".d.cts" : ".d.ts";
1155
+ const outputFileName = `${entryName}${dtsExtension}`;
1160
1156
  const tempBundledPath = join(tempOutputDir, outputFileName);
1161
1157
  const isMainEntry = "index" === entryName || 1 === entryPoints.size;
1162
1158
  const generateApiModel = apiModelEnabled;
@@ -1293,7 +1289,7 @@ async function bundleDtsFiles(options) {
1293
1289
  };
1294
1290
  }
1295
1291
  function stripSourceMapComment(content) {
1296
- return content.replace(/\/\/# sourceMappingURL=\S+\.d\.ts\.map\s*$/gm, "").trim();
1292
+ return content.replace(/\/\/# sourceMappingURL=\S+\.d\.c?ts\.map\s*$/gm, "").trim();
1297
1293
  }
1298
1294
  function mergeApiModels(options) {
1299
1295
  const { perEntryModels, packageName, exportPaths } = options;
@@ -1506,14 +1502,15 @@ function runTsgo(options) {
1506
1502
  let outputPath = file.relativePath;
1507
1503
  if (outputPath.startsWith("src/")) outputPath = outputPath.slice(4);
1508
1504
  if (".d.ts" !== dtsExtension && outputPath.endsWith(".d.ts")) outputPath = outputPath.replace(/\.d\.ts$/, dtsExtension);
1509
- const jsOutputPath = outputPath.replace(/\.d\.(ts|mts|cts)$/, ".js");
1505
+ const prefixedPath = options.dtsPathPrefix ? `${options.dtsPathPrefix}/${outputPath}` : outputPath;
1506
+ const jsOutputPath = prefixedPath.replace(/\.d\.(ts|mts|cts)$/, ".js");
1510
1507
  if (!context.compilation.assets[jsOutputPath]) continue;
1511
1508
  let content = await readFile(file.path, "utf-8");
1512
1509
  content = stripSourceMapComment(content);
1513
- const source = new context.sources.OriginalSource(content, outputPath);
1514
- context.compilation.emitAsset(outputPath, source);
1510
+ const source = new context.sources.OriginalSource(content, prefixedPath);
1511
+ context.compilation.emitAsset(prefixedPath, source);
1515
1512
  emittedCount++;
1516
- if (filesArray && outputPath.endsWith(".d.ts")) filesArray.add(outputPath);
1513
+ if (filesArray && prefixedPath.endsWith(".d.ts")) filesArray.add(prefixedPath);
1517
1514
  }
1518
1515
  core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted ${emittedCount} declaration file${1 === emittedCount ? "" : "s"} through asset pipeline`);
1519
1516
  }
@@ -1573,6 +1570,9 @@ function runTsgo(options) {
1573
1570
  },
1574
1571
  ...void 0 !== options.apiModel && {
1575
1572
  apiModel: options.apiModel
1573
+ },
1574
+ ...options.format && {
1575
+ format: options.format
1576
1576
  }
1577
1577
  });
1578
1578
  let apiModelPath;
@@ -1593,16 +1593,18 @@ function runTsgo(options) {
1593
1593
  await writeFile(mergedPath, JSON.stringify(mergedModel, null, 2), "utf-8");
1594
1594
  apiModelPath = mergedPath;
1595
1595
  }
1596
+ const bundledDtsExtension = "cjs" === options.format ? ".d.cts" : ".d.ts";
1596
1597
  if (options.bundle) {
1597
1598
  let emittedCount = 0;
1598
1599
  for (const [entryName, tempBundledPath] of bundledFiles){
1599
- const bundledFileName = `${entryName}.d.ts`;
1600
+ const bundledFileName = `${entryName}${bundledDtsExtension}`;
1601
+ const prefixedBundledFileName = options.dtsPathPrefix ? `${options.dtsPathPrefix}/${bundledFileName}` : bundledFileName;
1600
1602
  let content = await readFile(tempBundledPath, "utf-8");
1601
1603
  content = stripSourceMapComment(content);
1602
- const source = new context.sources.OriginalSource(content, bundledFileName);
1603
- context.compilation.emitAsset(bundledFileName, source);
1604
+ const source = new context.sources.OriginalSource(content, prefixedBundledFileName);
1605
+ context.compilation.emitAsset(prefixedBundledFileName, source);
1604
1606
  emittedCount++;
1605
- if (filesArray) filesArray.add(bundledFileName);
1607
+ if (filesArray) filesArray.add(prefixedBundledFileName);
1606
1608
  }
1607
1609
  core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted ${emittedCount} bundled declaration file${1 === emittedCount ? "" : "s"} through asset pipeline`);
1608
1610
  }
@@ -1624,7 +1626,7 @@ function runTsgo(options) {
1624
1626
  hasTsdocMetadata: !!tsdocMetadataPath,
1625
1627
  hasTsconfig: !!state.parsedConfig && !!state.tsconfigPath,
1626
1628
  cwd,
1627
- distPath: `dist/${envId}`
1629
+ distPath: `dist/${options.buildTarget ?? envId}`
1628
1630
  });
1629
1631
  }
1630
1632
  }
@@ -1653,15 +1655,17 @@ function runTsgo(options) {
1653
1655
  core_logger.info(`${picocolors.dim(`[${envId}]`)} Emitted resolved tsconfig: tsconfig.json (excluded from npm publish)`);
1654
1656
  }
1655
1657
  if (options.bundle) for (const [entryName] of bundledFiles){
1656
- const bundledFileName = `${entryName}.d.ts`;
1657
- const mapFileName = `${bundledFileName}.map`;
1658
+ const bundledFileName = `${entryName}${bundledDtsExtension}`;
1659
+ const prefixedBundledFileName = options.dtsPathPrefix ? `${options.dtsPathPrefix}/${bundledFileName}` : bundledFileName;
1660
+ const mapFileName = `${prefixedBundledFileName}.map`;
1658
1661
  for (const file of dtsFiles){
1659
1662
  if (file.relativePath.endsWith(".d.ts.map")) continue;
1660
1663
  let outputPath = file.relativePath;
1661
1664
  if (outputPath.startsWith("src/")) outputPath = outputPath.slice(4);
1662
1665
  if (".d.ts" !== dtsExtension && outputPath.endsWith(".d.ts")) outputPath = outputPath.replace(/\.d\.ts$/, dtsExtension);
1663
- const mapFileName = `${outputPath}.map`;
1664
- if (context.compilation.assets[mapFileName]) delete context.compilation.assets[mapFileName];
1666
+ const prefixedOutputPath = options.dtsPathPrefix ? `${options.dtsPathPrefix}/${outputPath}` : outputPath;
1667
+ const individualMapFileName = `${prefixedOutputPath}.map`;
1668
+ if (context.compilation.assets[individualMapFileName]) delete context.compilation.assets[individualMapFileName];
1665
1669
  }
1666
1670
  if (context.compilation.assets[mapFileName]) delete context.compilation.assets[mapFileName];
1667
1671
  }
@@ -1679,7 +1683,7 @@ function runTsgo(options) {
1679
1683
  stage: "summarize"
1680
1684
  }, async (compiler)=>{
1681
1685
  const assetsToDelete = [];
1682
- for(const assetName in compiler.compilation.assets)if (assetName.endsWith(".d.ts")) {
1686
+ for(const assetName in compiler.compilation.assets)if (assetName.endsWith(".d.ts") || assetName.endsWith(".d.cts")) {
1683
1687
  const asset = compiler.compilation.assets[assetName];
1684
1688
  const content = asset.source().toString();
1685
1689
  const strippedContent = stripSourceMapComment(content);
@@ -1687,7 +1691,7 @@ function runTsgo(options) {
1687
1691
  const source = new compiler.sources.OriginalSource(strippedContent, assetName);
1688
1692
  compiler.compilation.assets[assetName] = source;
1689
1693
  }
1690
- } else if (assetName.endsWith(".d.ts.map")) assetsToDelete.push(assetName);
1694
+ } else if (assetName.endsWith(".d.ts.map") || assetName.endsWith(".d.cts.map")) assetsToDelete.push(assetName);
1691
1695
  for (const assetName of assetsToDelete)delete compiler.compilation.assets[assetName];
1692
1696
  });
1693
1697
  api.onCloseBuild(async ()=>{
@@ -1830,6 +1834,7 @@ const FilesArrayPlugin = (options)=>({
1830
1834
  const license = await TextAsset.create(context, "LICENSE", false);
1831
1835
  if (license) filesArray.add(license.fileName);
1832
1836
  for (const assetName of Object.keys(context.compilation.assets))if (!assetName.endsWith(".map") && !filesArray.has(assetName)) filesArray.add(assetName);
1837
+ if (options?.formatDirs) for (const dir of options.formatDirs)filesArray.add(dir);
1833
1838
  if (options?.transformFiles) await options.transformFiles({
1834
1839
  compilation: context.compilation,
1835
1840
  filesArray,
@@ -1848,6 +1853,11 @@ const FilesArrayPlugin = (options)=>({
1848
1853
  ...previousFiles,
1849
1854
  ...Array.from(filesArray)
1850
1855
  ].sort());
1856
+ if (options?.formatDirs) {
1857
+ for (const file of [
1858
+ ...allFiles
1859
+ ])if (options.formatDirs.some((dir)=>file.startsWith(`${dir}/`))) allFiles.delete(file);
1860
+ }
1851
1861
  if (0 === allFiles.size) delete packageJson.data.files;
1852
1862
  else {
1853
1863
  const newFiles = new Set([
@@ -2118,7 +2128,7 @@ function createTypePath(jsPath, collapseIndex = true) {
2118
2128
  if (jsPath.endsWith(".js")) return `${jsPath.slice(0, -3)}.d.ts`;
2119
2129
  return `${jsPath}.d.ts`;
2120
2130
  }
2121
- function transformPackageBin(bin, _processTSExports = true) {
2131
+ function transformPackageBin(bin) {
2122
2132
  if ("string" == typeof bin) {
2123
2133
  if (bin.endsWith(".ts") || bin.endsWith(".tsx")) return "./bin/cli.js";
2124
2134
  return bin;
@@ -2184,7 +2194,7 @@ function applyRslibTransformations(packageJson, originalPackageJson, processTSEx
2184
2194
  };
2185
2195
  if (processedManifest.exports) processedManifest.exports = transformPackageExports(processedManifest.exports, processTSExports, void 0, entrypoints, exportToOutputMap, bundle ?? false);
2186
2196
  if (processedManifest.bin) {
2187
- const transformedBin = transformPackageBin(processedManifest.bin, processTSExports);
2197
+ const transformedBin = transformPackageBin(processedManifest.bin);
2188
2198
  if (transformedBin) processedManifest.bin = transformedBin;
2189
2199
  }
2190
2200
  if (originalPackageJson.typesVersions) {
@@ -2207,15 +2217,51 @@ async function applyPnpmTransformations(packageJson, dir = process.cwd(), catalo
2207
2217
  const workspaceCatalog = catalog ?? createWorkspaceCatalog();
2208
2218
  return workspaceCatalog.resolvePackageJson(packageJson, dir);
2209
2219
  }
2210
- async function buildPackageJson(packageJson, isProduction = false, processTSExports = true, entrypoints, exportToOutputMap, bundle, transform) {
2220
+ async function buildPackageJson(packageJson, isProduction = false, processTSExports = true, entrypoints, exportToOutputMap, bundle, transform, formatConditions) {
2211
2221
  let result;
2212
2222
  if (isProduction) {
2213
2223
  const pnpmTransformed = await applyPnpmTransformations(packageJson);
2214
2224
  result = applyRslibTransformations(pnpmTransformed, packageJson, processTSExports, entrypoints, exportToOutputMap, bundle);
2215
2225
  } else result = applyRslibTransformations(packageJson, packageJson, processTSExports, entrypoints, exportToOutputMap, bundle);
2226
+ if (formatConditions && result.exports) result.exports = applyFormatConditions(result.exports, formatConditions);
2216
2227
  if (transform) result = transform(result);
2217
2228
  return result;
2218
2229
  }
2230
+ function toCjsPath(jsPath) {
2231
+ if (jsPath.endsWith(".js")) return `${jsPath.slice(0, -3)}.cjs`;
2232
+ return jsPath;
2233
+ }
2234
+ function toCtsTypePath(dtsPath) {
2235
+ if (dtsPath.endsWith(".d.ts")) return `${dtsPath.slice(0, -5)}.d.cts`;
2236
+ return dtsPath;
2237
+ }
2238
+ function addFormatDirPrefix(path, format) {
2239
+ if (path.startsWith("./")) return `./${format}/${path.slice(2)}`;
2240
+ return `./${format}/${path}`;
2241
+ }
2242
+ function applyFormatConditions(exports, options) {
2243
+ if (!exports || "object" != typeof exports || Array.isArray(exports)) return exports;
2244
+ const { format = "esm", entryFormats, dualFormat } = options;
2245
+ const result = {};
2246
+ for (const [key, value] of Object.entries(exports))if (value && "object" == typeof value && !Array.isArray(value)) {
2247
+ const condObj = value;
2248
+ if ("string" == typeof condObj.types && "string" == typeof condObj.import) {
2249
+ const entryFormat = entryFormats?.[key] ?? format;
2250
+ const hasExplicitOverride = void 0 !== entryFormats && key in entryFormats;
2251
+ if (dualFormat && !hasExplicitOverride) result[key] = {
2252
+ types: addFormatDirPrefix(condObj.types, "esm"),
2253
+ import: addFormatDirPrefix(condObj.import, "esm"),
2254
+ require: addFormatDirPrefix(toCjsPath(condObj.import), "cjs")
2255
+ };
2256
+ else if ("cjs" === entryFormat) result[key] = {
2257
+ types: toCtsTypePath(condObj.types),
2258
+ require: toCjsPath(condObj.import)
2259
+ };
2260
+ else result[key] = value;
2261
+ } else result[key] = value;
2262
+ } else result[key] = value;
2263
+ return result;
2264
+ }
2219
2265
  const PackageJsonTransformPlugin = (options = {})=>({
2220
2266
  name: "package-json-processor",
2221
2267
  setup (api) {
@@ -2243,7 +2289,19 @@ const PackageJsonTransformPlugin = (options = {})=>({
2243
2289
  const isProduction = "dev" !== envId;
2244
2290
  const entrypoints = api.useExposed("entrypoints");
2245
2291
  const exportToOutputMap = api.useExposed("exportToOutputMap");
2246
- const processedPackageJson = await buildPackageJson(packageJson.data, isProduction, options.processTSExports, entrypoints, exportToOutputMap, options.bundle, options.transform);
2292
+ let formatConditions;
2293
+ if (options.entryFormats || options.dualFormat) formatConditions = {
2294
+ ...options.format && {
2295
+ format: options.format
2296
+ },
2297
+ ...options.entryFormats && {
2298
+ entryFormats: options.entryFormats
2299
+ },
2300
+ ...options.dualFormat && {
2301
+ dualFormat: options.dualFormat
2302
+ }
2303
+ };
2304
+ const processedPackageJson = await buildPackageJson(packageJson.data, isProduction, options.processTSExports, entrypoints, exportToOutputMap, options.bundle, options.transform, formatConditions);
2247
2305
  packageJson.data = processedPackageJson;
2248
2306
  if (options.forcePrivate) packageJson.data.private = true;
2249
2307
  if (options.format) packageJson.data.type = "esm" === options.format ? "module" : "commonjs";
@@ -2251,7 +2309,7 @@ const PackageJsonTransformPlugin = (options = {})=>({
2251
2309
  if (useRollupTypes && packageJson.data.exports && "object" == typeof packageJson.data.exports) {
2252
2310
  const exports = packageJson.data.exports;
2253
2311
  delete exports["./api-extractor"];
2254
- for (const [, value] of Object.entries(exports))if (value && "object" == typeof value && "types" in value) value.types = "./index.d.ts";
2312
+ for (const value of Object.values(exports))if (value && "object" == typeof value && "types" in value) value.types = "./index.d.ts";
2255
2313
  }
2256
2314
  packageJson.update();
2257
2315
  });
@@ -2684,6 +2742,9 @@ const VirtualEntryPlugin = (options)=>{
2684
2742
  },
2685
2743
  ...void 0 !== options.virtualEntries && {
2686
2744
  virtualEntries: options.virtualEntries
2745
+ },
2746
+ ...void 0 !== options.entryFormats && {
2747
+ entryFormats: options.entryFormats
2687
2748
  }
2688
2749
  };
2689
2750
  return merged;
@@ -2698,6 +2759,7 @@ const VirtualEntryPlugin = (options)=>{
2698
2759
  }
2699
2760
  static async createSingleTarget(target, opts) {
2700
2761
  const options = NodeLibraryBuilder.mergeOptions(opts);
2762
+ const bundle = options.bundle ?? true;
2701
2763
  const VERSION = await packageJsonVersion();
2702
2764
  const plugins = [];
2703
2765
  const apiModelConfig = "object" == typeof options.apiModel ? options.apiModel : {};
@@ -2714,7 +2776,7 @@ const VirtualEntryPlugin = (options)=>{
2714
2776
  };
2715
2777
  plugins.push(TsDocLintPlugin({
2716
2778
  ...lintOptions,
2717
- ...false === options.bundle && {
2779
+ ...!bundle && {
2718
2780
  perEntry: true
2719
2781
  }
2720
2782
  }));
@@ -2723,7 +2785,7 @@ const VirtualEntryPlugin = (options)=>{
2723
2785
  ...null != options.exportsAsIndexes && {
2724
2786
  exportsAsIndexes: options.exportsAsIndexes
2725
2787
  },
2726
- ...false === options.bundle && {
2788
+ ...!bundle && {
2727
2789
  bundleless: true
2728
2790
  }
2729
2791
  }));
@@ -2732,27 +2794,30 @@ const VirtualEntryPlugin = (options)=>{
2732
2794
  target,
2733
2795
  pkg
2734
2796
  }) : void 0;
2735
- const libraryFormat = options.format ?? "esm";
2736
- const collapseIndex = (options.bundle ?? true) || !(options.exportsAsIndexes ?? false);
2737
- plugins.push(PackageJsonTransformPlugin({
2738
- forcePrivate: "dev" === target,
2739
- bundle: collapseIndex,
2740
- target,
2741
- format: libraryFormat,
2742
- ...transformFn && {
2743
- transform: transformFn
2744
- }
2745
- }));
2746
- plugins.push(FilesArrayPlugin({
2747
- target,
2748
- ...options.transformFiles && {
2749
- transformFiles: options.transformFiles
2750
- }
2751
- }));
2752
- plugins.push(...options.plugins);
2753
- const outputDir = `dist/${target}`;
2797
+ const formatOption = options.format ?? "esm";
2798
+ const formats = Array.isArray(formatOption) ? formatOption : [
2799
+ formatOption
2800
+ ];
2801
+ const primaryFormat = formats[0] ?? "esm";
2802
+ const isDualFormat = formats.length > 1;
2803
+ const entryFormats = options.entryFormats;
2804
+ const hasFormatOverrides = void 0 !== entryFormats && Object.keys(entryFormats).length > 0;
2805
+ const collapseIndex = bundle || !(options.exportsAsIndexes ?? false);
2806
+ const baseOutputDir = `dist/${target}`;
2807
+ const apiModelForTarget = "npm" === target ? options.apiModel : void 0;
2808
+ const sourceMap = "dev" === target;
2809
+ const externalsConfig = options.externals && options.externals.length > 0 ? {
2810
+ externals: options.externals
2811
+ } : {};
2812
+ const bundlelessOutput = bundle ? {} : {
2813
+ legalComments: "inline"
2814
+ };
2815
+ const sourceDefine = {
2816
+ "process.env.__PACKAGE_VERSION__": JSON.stringify(VERSION),
2817
+ ...options.define
2818
+ };
2754
2819
  let entry = options.entry;
2755
- if (false === options.bundle && !entry) {
2820
+ if (!bundle && !entry) {
2756
2821
  const cwd = process.cwd();
2757
2822
  const packageJsonPath = join(cwd, "package.json");
2758
2823
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
@@ -2768,49 +2833,74 @@ const VirtualEntryPlugin = (options)=>{
2768
2833
  }
2769
2834
  entry = tracedEntries;
2770
2835
  }
2771
- const apiModelForTarget = "npm" === target ? options.apiModel : void 0;
2836
+ plugins.push(PackageJsonTransformPlugin({
2837
+ forcePrivate: "dev" === target,
2838
+ bundle: collapseIndex,
2839
+ target,
2840
+ format: primaryFormat,
2841
+ ...transformFn && {
2842
+ transform: transformFn
2843
+ },
2844
+ ...hasFormatOverrides && {
2845
+ entryFormats
2846
+ },
2847
+ ...isDualFormat && {
2848
+ dualFormat: true
2849
+ }
2850
+ }));
2851
+ plugins.push(FilesArrayPlugin({
2852
+ target,
2853
+ ...options.transformFiles && {
2854
+ transformFiles: options.transformFiles
2855
+ },
2856
+ ...isDualFormat && {
2857
+ formatDirs: formats
2858
+ }
2859
+ }));
2860
+ plugins.push(...options.plugins);
2772
2861
  plugins.push(DtsPlugin({
2773
2862
  ...options.tsconfigPath && {
2774
2863
  tsconfigPath: options.tsconfigPath
2775
2864
  },
2776
2865
  abortOnError: true,
2777
- bundle: options.bundle ?? true,
2866
+ bundle,
2778
2867
  ...options.dtsBundledPackages && {
2779
2868
  bundledPackages: options.dtsBundledPackages
2780
2869
  },
2781
2870
  buildTarget: target,
2782
- format: libraryFormat,
2871
+ format: primaryFormat,
2783
2872
  ...void 0 !== apiModelForTarget && {
2784
2873
  apiModel: apiModelForTarget
2874
+ },
2875
+ ...isDualFormat && {
2876
+ dtsPathPrefix: primaryFormat
2785
2877
  }
2786
2878
  }));
2787
2879
  const lib = {
2788
- id: target,
2789
- outBase: false === options.bundle ? "src" : outputDir,
2880
+ id: isDualFormat ? `${target}-${primaryFormat}` : target,
2881
+ outBase: bundle ? baseOutputDir : "src",
2790
2882
  output: {
2791
2883
  target: "node",
2792
2884
  module: true,
2793
2885
  cleanDistPath: true,
2794
- sourceMap: "dev" === target,
2795
- // Prevent @preserve comments from generating .LICENSE.txt files
2796
- ...false === options.bundle && {
2797
- legalComments: "inline"
2798
- },
2886
+ sourceMap,
2887
+ ...bundlelessOutput,
2799
2888
  distPath: {
2800
- root: outputDir
2889
+ root: baseOutputDir,
2890
+ ...isDualFormat && {
2891
+ js: primaryFormat
2892
+ }
2801
2893
  },
2802
2894
  copy: {
2803
2895
  patterns: options.copyPatterns
2804
2896
  },
2805
- ...options.externals && options.externals.length > 0 && {
2806
- externals: options.externals
2807
- }
2897
+ ...externalsConfig
2808
2898
  },
2809
- format: libraryFormat,
2899
+ format: primaryFormat,
2810
2900
  experiments: {
2811
- advancedEsm: "esm" === libraryFormat
2901
+ advancedEsm: "esm" === primaryFormat
2812
2902
  },
2813
- bundle: options.bundle ?? true,
2903
+ bundle,
2814
2904
  plugins,
2815
2905
  source: {
2816
2906
  ...options.tsconfigPath && {
@@ -2819,10 +2909,7 @@ const VirtualEntryPlugin = (options)=>{
2819
2909
  ...entry && {
2820
2910
  entry
2821
2911
  },
2822
- define: {
2823
- "process.env.__PACKAGE_VERSION__": JSON.stringify(VERSION),
2824
- ...options.define
2825
- }
2912
+ define: sourceDefine
2826
2913
  }
2827
2914
  };
2828
2915
  const hasRegularEntries = void 0 !== options.entry || NodeLibraryBuilder.packageHasExports();
@@ -2830,11 +2917,137 @@ const VirtualEntryPlugin = (options)=>{
2830
2917
  const hasVirtualEntries = Object.keys(virtualEntries).length > 0;
2831
2918
  if (!hasRegularEntries && !hasVirtualEntries) throw new Error("No entry points configured. Provide package.json exports, explicit entry option, or virtualEntries.");
2832
2919
  const libConfigs = [];
2833
- if (hasRegularEntries) libConfigs.push(lib);
2920
+ if (hasRegularEntries) {
2921
+ libConfigs.push(lib);
2922
+ if (isDualFormat) for (const secondaryFormat of formats.slice(1)){
2923
+ const secondaryPlugins = [
2924
+ {
2925
+ name: "strip-metadata-assets",
2926
+ setup (api) {
2927
+ api.processAssets({
2928
+ stage: "additional"
2929
+ }, (context)=>{
2930
+ for (const name of Object.keys(context.compilation.assets))if ("package.json" === name || "README.md" === name || "LICENSE" === name) delete context.compilation.assets[name];
2931
+ });
2932
+ }
2933
+ },
2934
+ DtsPlugin({
2935
+ ...options.tsconfigPath && {
2936
+ tsconfigPath: options.tsconfigPath
2937
+ },
2938
+ abortOnError: true,
2939
+ bundle,
2940
+ ...options.dtsBundledPackages && {
2941
+ bundledPackages: options.dtsBundledPackages
2942
+ },
2943
+ buildTarget: target,
2944
+ format: secondaryFormat,
2945
+ dtsPathPrefix: secondaryFormat
2946
+ })
2947
+ ];
2948
+ const secondaryLib = {
2949
+ id: `${target}-${secondaryFormat}`,
2950
+ outBase: bundle ? baseOutputDir : "src",
2951
+ output: {
2952
+ target: "node",
2953
+ cleanDistPath: false,
2954
+ sourceMap,
2955
+ ...bundlelessOutput,
2956
+ distPath: {
2957
+ root: baseOutputDir,
2958
+ js: secondaryFormat
2959
+ },
2960
+ ...externalsConfig
2961
+ },
2962
+ format: secondaryFormat,
2963
+ experiments: {
2964
+ advancedEsm: "esm" === secondaryFormat
2965
+ },
2966
+ bundle,
2967
+ plugins: secondaryPlugins,
2968
+ source: {
2969
+ ...options.tsconfigPath && {
2970
+ tsconfigPath: options.tsconfigPath
2971
+ },
2972
+ ...entry && {
2973
+ entry
2974
+ },
2975
+ define: sourceDefine
2976
+ }
2977
+ };
2978
+ libConfigs.push(secondaryLib);
2979
+ }
2980
+ if (hasFormatOverrides && !isDualFormat) {
2981
+ const cwd = process.cwd();
2982
+ const packageJsonPath = join(cwd, "package.json");
2983
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2984
+ const { entries: extractedEntries, exportPaths } = new EntryExtractor().extract(packageJson);
2985
+ const overridesByFormat = new Map();
2986
+ for (const [entryName, sourcePath] of Object.entries(extractedEntries)){
2987
+ const exportPath = exportPaths[entryName];
2988
+ if (exportPath && entryFormats?.[exportPath] && entryFormats[exportPath] !== primaryFormat) {
2989
+ const overrideFormat = entryFormats[exportPath];
2990
+ let formatEntries = overridesByFormat.get(overrideFormat);
2991
+ if (!formatEntries) {
2992
+ formatEntries = {};
2993
+ overridesByFormat.set(overrideFormat, formatEntries);
2994
+ }
2995
+ formatEntries[entryName] = sourcePath;
2996
+ }
2997
+ }
2998
+ for (const [overrideFormat, overrideEntries] of overridesByFormat){
2999
+ const overridePlugins = [
3000
+ FilesArrayPlugin({
3001
+ target
3002
+ }),
3003
+ DtsPlugin({
3004
+ ...options.tsconfigPath && {
3005
+ tsconfigPath: options.tsconfigPath
3006
+ },
3007
+ abortOnError: true,
3008
+ bundle,
3009
+ ...options.dtsBundledPackages && {
3010
+ bundledPackages: options.dtsBundledPackages
3011
+ },
3012
+ buildTarget: target,
3013
+ format: overrideFormat
3014
+ })
3015
+ ];
3016
+ const overrideLib = {
3017
+ id: `${target}-${overrideFormat}`,
3018
+ outBase: bundle ? baseOutputDir : "src",
3019
+ output: {
3020
+ target: "node",
3021
+ cleanDistPath: false,
3022
+ sourceMap,
3023
+ ...bundlelessOutput,
3024
+ distPath: {
3025
+ root: baseOutputDir
3026
+ },
3027
+ ...externalsConfig
3028
+ },
3029
+ format: overrideFormat,
3030
+ experiments: {
3031
+ advancedEsm: "esm" === overrideFormat
3032
+ },
3033
+ bundle,
3034
+ plugins: overridePlugins,
3035
+ source: {
3036
+ entry: overrideEntries,
3037
+ ...options.tsconfigPath && {
3038
+ tsconfigPath: options.tsconfigPath
3039
+ },
3040
+ define: sourceDefine
3041
+ }
3042
+ };
3043
+ libConfigs.push(overrideLib);
3044
+ }
3045
+ }
3046
+ }
2834
3047
  if (hasVirtualEntries) {
2835
3048
  const virtualByFormat = new Map();
2836
3049
  for (const [outputName, config] of Object.entries(virtualEntries)){
2837
- const entryFormat = config.format ?? libraryFormat;
3050
+ const entryFormat = config.format ?? primaryFormat;
2838
3051
  let formatMap = virtualByFormat.get(entryFormat);
2839
3052
  if (!formatMap) {
2840
3053
  formatMap = new Map();
@@ -2855,11 +3068,9 @@ const VirtualEntryPlugin = (options)=>{
2855
3068
  cleanDistPath: false,
2856
3069
  sourceMap: false,
2857
3070
  distPath: {
2858
- root: outputDir
3071
+ root: baseOutputDir
2859
3072
  },
2860
- ...options.externals && options.externals.length > 0 && {
2861
- externals: options.externals
2862
- }
3073
+ ...externalsConfig
2863
3074
  },
2864
3075
  source: {
2865
3076
  entry: entryMap
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/rslib-builder",
3
- "version": "0.12.2",
3
+ "version": "0.13.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": [
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/savvy-web/rslib-builder.git"
28
+ "url": "git+https://github.com/savvy-web/rslib-builder.git"
29
29
  },
30
30
  "license": "MIT",
31
31
  "author": {
@@ -63,8 +63,8 @@
63
63
  "peerDependencies": {
64
64
  "@microsoft/api-extractor": "^7.56.3",
65
65
  "@rslib/core": "^0.19.5",
66
- "@types/node": "^25.2.2",
67
- "@typescript/native-preview": "^7.0.0-dev.20260209.1",
66
+ "@types/node": "^25.2.3",
67
+ "@typescript/native-preview": "7.0.0-dev.20260210.1",
68
68
  "typescript": "^5.9.3"
69
69
  },
70
70
  "peerDependenciesMeta": {