@d1g1tal/tsbuild 1.8.2 → 1.8.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## [1.8.4](https://github.com/D1g1talEntr0py/tsbuild/compare/v1.8.3...v1.8.4) (2026-04-12)
2
+
3
+ ### Performance Improvements
4
+
5
+ * **build:** delegate file writing to esbuild (84a64e212a9dc7f309bb2dcbe74ffa990c06065b)
6
+ - Enables esbuild write option for direct-to-disk transpilation output
7
+ - Moves relative module specifier rewriting logic to FileManager
8
+ - Simplifies output plugin to only manage shebang executable permissions via chmod
9
+ - Adapts IIFE plugin to process metafile outputs instead of in-memory files
10
+ - Updates tests and mock helpers to align with file writing capabilities
11
+
12
+ * **dts:** optimize declaration bundler module resolution and graph traversal (59606c0ccfe54ac74068fe6ab8f6d26d7edd97b6)
13
+ - Optimizes module pattern matching by using Sets and RegEx arrays instead of iteration
14
+ - Refactors bundled specifiers state to use ReadonlySet for O(1) lookups
15
+ - Simplifies deduplication of non-mergeable imports using Sets
16
+ - Yields the event loop before cpu-intensive declaration bundling to prevent I/O blocking
17
+
18
+
19
+ ### Miscellaneous Chores
20
+
21
+ * **perf:** add performance baseline documentation and benchmark script (3e897aaa7a35022b8d9ec1d4222448973eb57a2b)
22
+ - Adds documentation detailing performance baselines and architectures
23
+ - Introduces performance measurements log
24
+ - Adds quick reference for performance monitoring
25
+ - Introduces new benchmark script for running automated metrics collection
26
+ - Registers bench script in package.json
27
+
28
+ ## [1.8.3](https://github.com/D1g1talEntr0py/tsbuild/compare/v1.8.2...v1.8.3) (2026-04-11)
29
+
30
+ ### Performance Improvements
31
+
32
+ * **core:** optimize build performance and decrease startup time (5cc35a8e0c6acc45dff38f6186e9f5b8f59ee8bf)
33
+ - use sets instead of arrays for faster lookups
34
+ - replace string concatenation with array joins
35
+ - replace object.keys with for-in loops
36
+ - cache regex patterns and avoid repeated compilations
37
+ - dynamically import esbuild and watchr to reduce startup time
38
+ - replace map/reduce chains with single loops for better performance
39
+
40
+
41
+ ### Miscellaneous Chores
42
+
43
+ * **deps:** update dependency @types/node to ^25.6.0 (a99b720acf5c2d3ed4116605e66fbd0f0174259f)
44
+
1
45
  ## [1.8.2](https://github.com/D1g1talEntr0py/tsbuild/compare/v1.8.1...v1.8.2) (2026-04-09)
2
46
 
3
47
  ### Bug Fixes
@@ -139,9 +139,6 @@ var Files = class _Files {
139
139
  }
140
140
  };
141
141
 
142
- // src/type-script-project.ts
143
- import { Watchr } from "@d1g1tal/watchr";
144
-
145
142
  // src/text-formatter.ts
146
143
  import { isatty } from "node:tty";
147
144
  var { env = {}, platform = "" } = process;
@@ -151,13 +148,13 @@ var isColorSupported = !("NO_COLOR" in env) && ("FORCE_COLOR" in env || platform
151
148
  var replaceClose = (index, string, close, replace, head = string.substring(0, index) + replace, tail = string.substring(index + close.length), next = tail.indexOf(close)) => {
152
149
  return head + (next < 0 ? tail : replaceClose(next, tail, close, replace));
153
150
  };
154
- var clearBleed = (index, string, open, close, replace) => {
155
- return index < 0 ? `${open}${string}${close}` : `${open}${replaceClose(index, string, close, replace)}${close}`;
151
+ var clearBleed = (index, string, open2, close, replace) => {
152
+ return index < 0 ? `${open2}${string}${close}` : `${open2}${replaceClose(index, string, close, replace)}${close}`;
156
153
  };
157
- var filterEmpty = (open, close, replace = open, at = open.length + 1) => {
158
- return (text) => text.length ? clearBleed(text.indexOf(close, at), text, open, close, replace) : "";
154
+ var filterEmpty = (open2, close, replace = open2, at = open2.length + 1) => {
155
+ return (text) => text.length ? clearBleed(text.indexOf(close, at), text, open2, close, replace) : "";
159
156
  };
160
- var generateTextFormatter = (open, close, replace) => filterEmpty(`\x1B[${open}m`, `\x1B[${close}m`, replace);
157
+ var generateTextFormatter = (open2, close, replace) => filterEmpty(`\x1B[${open2}m`, `\x1B[${close}m`, replace);
161
158
  var TextFormat = class {
162
159
  static enabled = isColorSupported;
163
160
  static reset = generateTextFormatter(0, 0);
@@ -204,6 +201,8 @@ var TextFormat = class {
204
201
  };
205
202
 
206
203
  // src/logger.ts
204
+ var LOG_1024 = Math.log(1024);
205
+ var ansiEscapePattern = /\x1b\[[0-9;]*m/g;
207
206
  var isWrittenFiles = (data) => {
208
207
  if (!Array.isArray(data)) {
209
208
  return false;
@@ -231,7 +230,7 @@ var prettyBytes = (bytes) => {
231
230
  if (bytes === 0) {
232
231
  return { value: "0", unit: "B" };
233
232
  }
234
- const exp = ~~(Math.log(bytes) / Math.log(1024));
233
+ const exp = ~~(Math.log(bytes) / LOG_1024);
235
234
  return { value: (bytes / Math.pow(1024, exp)).toFixed(2), unit: dataUnits[exp] };
236
235
  };
237
236
  var Logger = class _Logger {
@@ -255,7 +254,7 @@ var Logger = class _Logger {
255
254
  * @param message The message to display in the header.
256
255
  */
257
256
  static header(message) {
258
- const innerWidth = message.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "").length + 2;
257
+ const innerWidth = message.replace(ansiEscapePattern, "").length + 2;
259
258
  console.log(TextFormat.cyan(`\u256D${"\u2500".repeat(innerWidth)}\u256E${newLine}\u2502 ${message} \u2502${newLine}\u2570${"\u2500".repeat(innerWidth)}\u256F`));
260
259
  }
261
260
  /**
@@ -283,8 +282,17 @@ var Logger = class _Logger {
283
282
  if (visible.length === 0) {
284
283
  return;
285
284
  }
286
- const maxNameLength = visible.reduce((max, { name }) => Math.max(max, name.length), 0);
287
- const maxDurationLength = visible.reduce((max, { duration }) => Math.max(max, duration.length), 0);
285
+ let maxNameLength = 0;
286
+ let maxDurationLength = 0;
287
+ for (let i = 0, length = visible.length; i < length; i++) {
288
+ const { name, duration } = visible[i];
289
+ if (name.length > maxNameLength) {
290
+ maxNameLength = name.length;
291
+ }
292
+ if (duration.length > maxDurationLength) {
293
+ maxDurationLength = duration.length;
294
+ }
295
+ }
288
296
  for (let i = 0, length = visible.length; i < length; i++) {
289
297
  const { name, duration } = visible[i];
290
298
  const prefix = i === length - 1 ? " \u2514\u2500" : " \u251C\u2500";
@@ -353,10 +361,24 @@ var Logger = class _Logger {
353
361
  * @internal
354
362
  */
355
363
  static files(files) {
356
- const maxPathLength = files.reduce((max, { path }) => Math.max(max, path.length), 0);
357
- const formatted = files.map(({ path, size }) => ({ path, ...prettyBytes(size) }));
358
- const maxValueLength = formatted.reduce((max, { value }) => Math.max(max, value.length), 0);
359
- const maxUnitLength = formatted.reduce((max, { unit }) => Math.max(max, unit.length), 0);
364
+ let maxPathLength = 0;
365
+ let maxValueLength = 0;
366
+ let maxUnitLength = 0;
367
+ const formatted = [];
368
+ for (let i = 0, length = files.length; i < length; i++) {
369
+ const { path, size } = files[i];
370
+ const { value, unit } = prettyBytes(size);
371
+ if (path.length > maxPathLength) {
372
+ maxPathLength = path.length;
373
+ }
374
+ if (value.length > maxValueLength) {
375
+ maxValueLength = value.length;
376
+ }
377
+ if (unit.length > maxUnitLength) {
378
+ maxUnitLength = unit.length;
379
+ }
380
+ formatted.push({ path, value, unit });
381
+ }
360
382
  for (let i = 0, length = formatted.length; i < length; i++) {
361
383
  const { path, value, unit } = formatted[i];
362
384
  const paddedPath = path.padEnd(maxPathLength);
@@ -536,17 +558,13 @@ var DeclarationProcessor = class _DeclarationProcessor {
536
558
  function createNamespaceImport(fileId) {
537
559
  let importName = inlineImports.get(fileId);
538
560
  if (importName === void 0) {
539
- let sanitized = "";
561
+ const chars = [];
540
562
  for (let i = 0; i < fileId.length; i++) {
541
563
  const char = fileId[i];
542
564
  const code2 = char.charCodeAt(0);
543
- if (code2 >= 97 && code2 <= 122 || code2 >= 65 && code2 <= 90 || code2 >= 48 && code2 <= 57 || code2 === 95 || code2 === 36) {
544
- sanitized += char;
545
- } else {
546
- sanitized += "_";
547
- }
565
+ chars.push(code2 >= 97 && code2 <= 122 || code2 >= 65 && code2 <= 90 || code2 >= 48 && code2 <= 57 || code2 === 95 || code2 === 36 ? char : "_");
548
566
  }
549
- importName = generateUniqueName(sanitized);
567
+ importName = generateUniqueName(chars.join(""));
550
568
  inlineImports.set(fileId, importName);
551
569
  }
552
570
  return importName;
@@ -799,36 +817,40 @@ import {
799
817
  forEachChild as forEachChild2
800
818
  } from "typescript";
801
819
  var nodeModules = "/node_modules/";
820
+ var emptySet = /* @__PURE__ */ new Set();
802
821
  var importPattern = /^import\s*(?:type\s*)?\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]\s*;?\s*$/;
803
822
  var typePrefixPattern = /^type:/;
804
823
  function mergeImports(imports) {
805
824
  const moduleImports = /* @__PURE__ */ new Map();
806
- const nonMergeableImports = [];
825
+ const nonMergeableImports = /* @__PURE__ */ new Set();
807
826
  for (const importStatement of imports) {
808
827
  const match = importPattern.exec(importStatement);
809
828
  if (match) {
810
829
  const [, namesString, moduleSpecifier] = match;
811
830
  const isType = importStatement.includes("import type");
812
831
  const key = `${isType ? "type:" : ""}${moduleSpecifier}`;
813
- if (!moduleImports.has(key)) {
814
- moduleImports.set(key, { names: /* @__PURE__ */ new Set(), isType });
832
+ let entry = moduleImports.get(key);
833
+ if (entry === void 0) {
834
+ entry = { names: /* @__PURE__ */ new Set(), isType };
835
+ moduleImports.set(key, entry);
815
836
  }
816
- const entry = moduleImports.get(key);
817
837
  for (const name of namesString.split(",")) {
818
838
  entry.names.add(name.trim());
819
839
  }
820
840
  } else {
821
- nonMergeableImports.push(importStatement);
841
+ nonMergeableImports.add(importStatement);
822
842
  }
823
843
  }
824
844
  const result = [];
825
845
  for (const [key, { names, isType }] of moduleImports) {
826
846
  result.push(`${isType ? "import type" : "import"} { ${[...names].sort().join(", ")} } from "${key.replace(typePrefixPattern, "")}";`);
827
847
  }
828
- result.push(...[...new Set(nonMergeableImports)]);
848
+ for (const imp of nonMergeableImports) {
849
+ result.push(imp);
850
+ }
829
851
  return result;
830
852
  }
831
- var DeclarationBundler = class {
853
+ var DeclarationBundler = class _DeclarationBundler {
832
854
  /** Project declaration files from in-memory FileManager */
833
855
  declarationFiles = /* @__PURE__ */ new Map();
834
856
  /** External declaration files resolved from disk (node_modules) when resolve is enabled */
@@ -841,6 +863,10 @@ var DeclarationBundler = class {
841
863
  moduleResolutionCache = /* @__PURE__ */ new Map();
842
864
  /** Pre-computed set of directory prefixes from declaration file paths for O(1) directoryExists lookups */
843
865
  declarationDirs = /* @__PURE__ */ new Set();
866
+ /** Pre-built matcher for external patterns — O(1) string lookups + cached regex tests */
867
+ matchExternal;
868
+ /** Pre-built matcher for noExternal patterns — O(1) string lookups + cached regex tests */
869
+ matchNoExternal;
844
870
  // Create a proper module resolution host that supports both in-memory files and disk files
845
871
  moduleResolutionHost = {
846
872
  fileExists: (fileName) => {
@@ -890,6 +916,8 @@ var DeclarationBundler = class {
890
916
  }
891
917
  }
892
918
  this.options = dtsBundleOptions;
919
+ this.matchExternal = _DeclarationBundler.buildMatcher(dtsBundleOptions.external);
920
+ this.matchNoExternal = _DeclarationBundler.buildMatcher(dtsBundleOptions.noExternal);
893
921
  }
894
922
  /**
895
923
  * Clears external declaration files and module resolution cache to free memory.
@@ -942,23 +970,42 @@ var DeclarationBundler = class {
942
970
  return imports;
943
971
  }
944
972
  /**
945
- * Check if a module specifier matches a pattern list
946
- * @param moduleSpecifier - The module specifier to check
947
- * @param patterns - Array of patterns to match against
948
- * @returns True if the module matches any pattern
973
+ * Builds an O(1) matcher from a mixed Pattern array by splitting into a Set<string> for
974
+ * exact/sub-path checks and a RegExp[] for regex tests. Called once per bundler instance.
975
+ * @param patterns - The array of string and RegExp patterns to match against module specifiers
976
+ * @returns A function that takes a module specifier and returns true if it matches any of the patterns
949
977
  */
950
- matchesPattern(moduleSpecifier, patterns) {
951
- return patterns.some((pattern) => {
952
- return typeof pattern === "string" ? moduleSpecifier === pattern || moduleSpecifier.startsWith(`${pattern}/`) : pattern.test(moduleSpecifier);
953
- });
954
- }
955
- /**
956
- * Check if a module specifier matches explicit external patterns
957
- * @param moduleSpecifier - The module specifier to check
958
- * @returns True if the module should be treated as external
959
- */
960
- isExternal(moduleSpecifier) {
961
- return this.matchesPattern(moduleSpecifier, this.options.external);
978
+ static buildMatcher(patterns) {
979
+ const exact = /* @__PURE__ */ new Set();
980
+ const prefixes = [];
981
+ const regexps = [];
982
+ for (const p of patterns) {
983
+ if (typeof p === "string") {
984
+ exact.add(p);
985
+ prefixes.push(p + "/");
986
+ } else {
987
+ regexps.push(p);
988
+ }
989
+ }
990
+ if (exact.size === 0 && regexps.length === 0) {
991
+ return () => false;
992
+ }
993
+ return (id) => {
994
+ if (exact.has(id)) {
995
+ return true;
996
+ }
997
+ for (let i = 0; i < prefixes.length; i++) {
998
+ if (id.startsWith(prefixes[i])) {
999
+ return true;
1000
+ }
1001
+ }
1002
+ for (let i = 0; i < regexps.length; i++) {
1003
+ if (regexps[i].test(id)) {
1004
+ return true;
1005
+ }
1006
+ }
1007
+ return false;
1008
+ };
962
1009
  }
963
1010
  /**
964
1011
  * Resolve a module import using TypeScript's resolution algorithm with path mapping support
@@ -1006,18 +1053,18 @@ var DeclarationBundler = class {
1006
1053
  const sourceFile = createSourceFile(path, code, ScriptTarget.Latest, true);
1007
1054
  const identifiers = this.collectIdentifiers(sourceFile.statements, sourceFile);
1008
1055
  const module = { path, code, imports: /* @__PURE__ */ new Set(), typeReferences: new Set(typeReferences), fileReferences: new Set(fileReferences), sourceFile, identifiers };
1009
- const bundledSpecs = [];
1056
+ const bundledSpecs = /* @__PURE__ */ new Set();
1010
1057
  for (const specifier of this.extractImports(sourceFile)) {
1011
- if (this.isExternal(specifier)) {
1058
+ if (this.matchExternal(specifier)) {
1012
1059
  continue;
1013
1060
  }
1014
1061
  const resolvedPath = this.resolveModule(specifier, path);
1015
- if (resolvedPath?.includes(nodeModules) && !this.matchesPattern(specifier, this.options.noExternal)) {
1062
+ if (resolvedPath?.includes(nodeModules) && !this.matchNoExternal(specifier)) {
1016
1063
  continue;
1017
1064
  }
1018
1065
  if (resolvedPath && (this.declarationFiles.has(resolvedPath) || this.externalDeclarationFiles.has(resolvedPath))) {
1019
1066
  module.imports.add(resolvedPath);
1020
- bundledSpecs.push(specifier);
1067
+ bundledSpecs.add(specifier);
1021
1068
  visit(resolvedPath);
1022
1069
  }
1023
1070
  }
@@ -1128,7 +1175,7 @@ var DeclarationBundler = class {
1128
1175
  * @param code - Declaration file content
1129
1176
  * @param sourceFile - Parsed source file AST (required to avoid re-parsing)
1130
1177
  * @param identifiers - Pre-computed type and value identifiers (to avoid re-computation)
1131
- * @param bundledImportPaths - Array of resolved file paths that were bundled (to exclude from external imports)
1178
+ * @param bundledImportPaths - Set of resolved file paths that were bundled (to exclude from external imports)
1132
1179
  * @param renameMap - Map of renamed identifiers (name:path -> newName)
1133
1180
  * @param modulePath - Path of current module for looking up renames
1134
1181
  * @returns Object with processed code, collected external imports, and exported names (separated by type/value)
@@ -1151,7 +1198,7 @@ var DeclarationBundler = class {
1151
1198
  for (const statement of sourceFile.statements) {
1152
1199
  if (isImportDeclaration2(statement)) {
1153
1200
  const moduleSpecifier = statement.moduleSpecifier.text;
1154
- if (this.isExternal(moduleSpecifier) || !bundledImportPaths.includes(moduleSpecifier)) {
1201
+ if (this.matchExternal(moduleSpecifier) || !bundledImportPaths.has(moduleSpecifier)) {
1155
1202
  externalImports.push(code.substring(statement.pos, statement.end).trim());
1156
1203
  } else if (statement.importClause?.namedBindings && isNamespaceImport(statement.importClause.namedBindings)) {
1157
1204
  bundledNamespaceAliases.add(statement.importClause.namedBindings.name.text);
@@ -1206,8 +1253,9 @@ var DeclarationBundler = class {
1206
1253
  };
1207
1254
  forEachChild2(sourceFile, visitQualified);
1208
1255
  }
1209
- const finalValueExports = [...new Set(valueExports.map(exportsMapper))];
1210
- const finalTypeExports = [...new Set(typeExports.map(exportsMapper).filter((t) => !new Set(finalValueExports).has(t)))];
1256
+ const finalValueExportsSet = new Set(valueExports.map(exportsMapper));
1257
+ const finalValueExports = [...finalValueExportsSet];
1258
+ const finalTypeExports = [...new Set(typeExports.map(exportsMapper).filter((t) => !finalValueExportsSet.has(t)))];
1211
1259
  return { code: magic.toString(), externalImports, typeExports: finalTypeExports, valueExports: finalValueExports };
1212
1260
  }
1213
1261
  /**
@@ -1243,7 +1291,9 @@ var DeclarationBundler = class {
1243
1291
  for (const [name, sourcesSet] of declarationSources) {
1244
1292
  if (sourcesSet.size > 1) {
1245
1293
  let suffix = 1;
1246
- for (const modulePath of Array.from(sourcesSet).slice(1)) {
1294
+ const modulePaths = sourcesSet.values();
1295
+ modulePaths.next();
1296
+ for (const modulePath of modulePaths) {
1247
1297
  let candidate = `${name}$${suffix}`;
1248
1298
  while (declarationSources.has(candidate)) {
1249
1299
  candidate = `${name}$${++suffix}`;
@@ -1256,7 +1306,7 @@ var DeclarationBundler = class {
1256
1306
  for (const { path, typeReferences, fileReferences, sourceFile, code, identifiers } of sortedModules) {
1257
1307
  allTypeReferences.push(...typeReferences);
1258
1308
  allFileReferences.push(...fileReferences);
1259
- const bundledForThisModule = bundledSpecifiers.get(path) || [];
1309
+ const bundledForThisModule = bundledSpecifiers.get(path) ?? emptySet;
1260
1310
  const { code: strippedCode, externalImports, typeExports, valueExports } = this.stripImportsExports(code, sourceFile, identifiers, bundledForThisModule, renameMap, path);
1261
1311
  allExternalImports.push(...externalImports);
1262
1312
  if (!path.includes(nodeModules)) {
@@ -1282,8 +1332,8 @@ var DeclarationBundler = class {
1282
1332
  }
1283
1333
  }
1284
1334
  const uniqueTypeReferences = [...typeReferencesSet];
1285
- const finalValueExports = [...new Set(allValueExports)];
1286
- const finalValueExportsSet = new Set(finalValueExports);
1335
+ const finalValueExportsSet = new Set(allValueExports);
1336
+ const finalValueExports = [...finalValueExportsSet];
1287
1337
  const finalTypeExports = [...new Set(allTypeExports.filter((typeExport) => !finalValueExportsSet.has(typeExport)))];
1288
1338
  const outputParts = [];
1289
1339
  if (uniqueFileReferences.length > 0) {
@@ -1353,11 +1403,12 @@ async function bundleDeclarations(options) {
1353
1403
  await mkdir2(options.compilerOptions.outDir, defaultDirOptions);
1354
1404
  }
1355
1405
  const dtsBundler = new DeclarationBundler(options);
1406
+ await new Promise((resolve3) => void setImmediate(resolve3));
1356
1407
  const bundleTasks = [];
1357
1408
  for (const [entryName, entryPoint] of Object.entries(options.entryPoints)) {
1358
1409
  const content = dtsBundler.bundle(entryPoint);
1359
1410
  if (content.length > 0) {
1360
- const outPath = Paths.join(options.compilerOptions.outDir, `${entryName}.d.ts`);
1411
+ const outPath = Paths.join(options.compilerOptions.outDir, `${entryName}${FileExtension.DTS}`);
1361
1412
  bundleTasks.push(writeFile2(outPath, content, Encoding.utf8).then(() => ({ path: Paths.relative(options.currentDirectory, outPath), size: content.length })));
1362
1413
  }
1363
1414
  }
@@ -1367,38 +1418,42 @@ async function bundleDeclarations(options) {
1367
1418
  }
1368
1419
 
1369
1420
  // src/plugins/output.ts
1421
+ import { chmod, open } from "node:fs/promises";
1370
1422
  import { extname } from "node:path";
1371
- var textEncoder = new TextEncoder();
1372
- var textDecoder = new TextDecoder();
1373
- var localFileIdentifier = /\.[a-z]+$/i;
1374
- var relativeSpecifierPattern = /(from\s*['"])(\.\.?\/[^'"]*?)(['"])/g;
1375
- var FileMode = { READ_WRITE: 438, READ_WRITE_EXECUTE: 493 };
1376
- function rewriteRelativeSpecifiers(code) {
1377
- return code.replace(relativeSpecifierPattern, (_, before, path, after) => localFileIdentifier.test(path) ? before + path + after : `${before}${path}.js${after}`);
1378
- }
1379
- async function fileMapper({ path, contents }) {
1380
- if (extname(path) !== FileExtension.JS) {
1381
- return Files.write(path, contents, { mode: FileMode.READ_WRITE });
1382
- }
1383
- let rewritten = false;
1384
- const result = textDecoder.decode(contents).replace(relativeSpecifierPattern, (_, before, specPath, after) => {
1385
- if (localFileIdentifier.test(specPath)) {
1386
- return before + specPath + after;
1423
+ async function setShebangPermissions(filePath) {
1424
+ const handle = await open(filePath, "r");
1425
+ try {
1426
+ const buf = Buffer.alloc(2);
1427
+ await handle.read(buf, 0, 2, 0);
1428
+ if (buf[0] === 35 && buf[1] === 33) {
1429
+ await chmod(filePath, 493);
1387
1430
  }
1388
- rewritten = true;
1389
- return `${before}${specPath}.js${after}`;
1390
- });
1391
- return Files.write(path, rewritten ? textEncoder.encode(result) : contents, { mode: contents[0] === 35 && contents[1] === 33 ? FileMode.READ_WRITE_EXECUTE : FileMode.READ_WRITE });
1431
+ } finally {
1432
+ await handle.close();
1433
+ }
1392
1434
  }
1393
1435
  var outputPlugin = () => {
1394
1436
  return {
1395
1437
  name: "esbuild:output-plugin",
1396
1438
  /**
1397
- * Configures the esbuild build instance to write output files to disk
1439
+ * Checks JS entry points for shebangs and sets executable permissions
1398
1440
  * @param build The esbuild build instance
1399
1441
  */
1400
1442
  setup(build) {
1401
- build.onEnd(async ({ outputFiles }) => void await Promise.all(outputFiles.map(fileMapper)));
1443
+ build.onEnd(async ({ metafile }) => {
1444
+ if (!metafile) {
1445
+ return;
1446
+ }
1447
+ const tasks = [];
1448
+ for (const [outputPath, { entryPoint }] of Object.entries(metafile.outputs)) {
1449
+ if (entryPoint && extname(outputPath) === FileExtension.JS) {
1450
+ tasks.push(setShebangPermissions(outputPath));
1451
+ }
1452
+ }
1453
+ if (tasks.length > 0) {
1454
+ await Promise.all(tasks);
1455
+ }
1456
+ });
1402
1457
  }
1403
1458
  };
1404
1459
  };
@@ -1519,10 +1574,9 @@ async function resolvePlugins(plugins, projectDir) {
1519
1574
  }
1520
1575
 
1521
1576
  // src/plugins/iife.ts
1522
- import { basename as basename2, dirname as dirname2, join, relative, resolve as resolve2 } from "node:path";
1523
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
1524
- import { build as esbuild } from "esbuild";
1525
- var textDecoder2 = new TextDecoder();
1577
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
1578
+ import { basename as basename2, dirname as dirname2, join, resolve as resolve2 } from "node:path";
1579
+ var textDecoder = new TextDecoder();
1526
1580
  var jsExtension = ".js";
1527
1581
  function iifePlugin(options) {
1528
1582
  const globalName = options?.globalName;
@@ -1542,11 +1596,11 @@ function iifePlugin(options) {
1542
1596
  }
1543
1597
  const sourcemap = build.initialOptions.sourcemap;
1544
1598
  const entryPointNames = extractEntryNames(build.initialOptions.entryPoints);
1545
- build.onEnd(async ({ outputFiles }) => {
1546
- if (!outputFiles?.length || entryPointNames.length === 0) {
1599
+ build.onEnd(async ({ metafile }) => {
1600
+ if (!metafile || entryPointNames.length === 0) {
1547
1601
  return;
1548
1602
  }
1549
- const written = await buildIife(outputFiles, entryPointNames, outdir, globalName, sourcemap);
1603
+ const written = await buildIife(metafile.outputs, entryPointNames, outdir, globalName, sourcemap);
1550
1604
  files.push(...written);
1551
1605
  });
1552
1606
  }
@@ -1570,15 +1624,15 @@ function extractEntryNames(entryPoints) {
1570
1624
  }
1571
1625
  return Object.keys(entryPoints);
1572
1626
  }
1573
- var exportRe = /export\s*\{([^}]*)\};?/g;
1574
- var asRe = /^(\w+)\s+as\s+(\w+)$/;
1627
+ var exportRegex = /export\s*\{([^}]*)\};?/g;
1628
+ var exportAliasRegex = /^(\w+)\s+as\s+(\w+)$/;
1575
1629
  function wrapAsIife(text, globalName) {
1576
- let last = null;
1577
- let match;
1578
- exportRe.lastIndex = 0;
1579
- while ((match = exportRe.exec(text)) !== null) {
1580
- last = match;
1630
+ const exportIndex = text.lastIndexOf("export");
1631
+ if (exportIndex === -1) {
1632
+ return text;
1581
1633
  }
1634
+ exportRegex.lastIndex = exportIndex;
1635
+ const last = exportRegex.exec(text);
1582
1636
  if (!last) {
1583
1637
  return text;
1584
1638
  }
@@ -1588,7 +1642,7 @@ function wrapAsIife(text, globalName) {
1588
1642
  if (!trimmed) {
1589
1643
  continue;
1590
1644
  }
1591
- const m = asRe.exec(trimmed);
1645
+ const m = exportAliasRegex.exec(trimmed);
1592
1646
  props.push(m ? `${m[2]}: ${m[1]}` : trimmed);
1593
1647
  }
1594
1648
  if (props.length === 0) {
@@ -1602,11 +1656,12 @@ ${body}
1602
1656
  ${assignment}
1603
1657
  })();${after}`;
1604
1658
  }
1605
- async function buildIife(outputFiles, entryPointNames, outdir, globalName, sourcemap) {
1659
+ async function buildIife(outputs, entryPointNames, outdir, globalName, sourcemap) {
1660
+ const { build: esbuild } = await import("esbuild");
1606
1661
  const fileContents = /* @__PURE__ */ new Map();
1607
- for (const file of outputFiles) {
1608
- if (file.path.endsWith(jsExtension)) {
1609
- fileContents.set(file.path, textDecoder2.decode(file.contents));
1662
+ for (const outputPath of Object.keys(outputs)) {
1663
+ if (outputPath.endsWith(jsExtension)) {
1664
+ fileContents.set(outputPath, await readFile2(outputPath, "utf8"));
1610
1665
  }
1611
1666
  }
1612
1667
  const validEntries = [];
@@ -1638,15 +1693,16 @@ async function buildIife(outputFiles, entryPointNames, outdir, globalName, sourc
1638
1693
  ));
1639
1694
  const written = [];
1640
1695
  const writes = [];
1696
+ const cwd = process.cwd();
1641
1697
  for (const { outputFiles: iifeFiles } of results) {
1642
- for (const file of iifeFiles) {
1643
- if (file.path.endsWith(jsExtension)) {
1644
- const text = wrapAsIife(textDecoder2.decode(file.contents), globalName);
1645
- writes.push(writeFile3(file.path, text));
1646
- written.push({ path: relative(process.cwd(), file.path), size: Buffer.byteLength(text) });
1698
+ for (const { path, contents } of iifeFiles) {
1699
+ if (path.endsWith(jsExtension)) {
1700
+ const text = wrapAsIife(textDecoder.decode(contents), globalName);
1701
+ writes.push(writeFile3(path, text));
1702
+ written.push({ path: Paths.relative(cwd, path), size: Buffer.byteLength(text) });
1647
1703
  } else {
1648
- writes.push(writeFile3(file.path, file.contents));
1649
- written.push({ path: relative(process.cwd(), file.path), size: file.contents.byteLength });
1704
+ writes.push(writeFile3(path, contents));
1705
+ written.push({ path: Paths.relative(cwd, path), size: contents.byteLength });
1650
1706
  }
1651
1707
  }
1652
1708
  }
@@ -1921,6 +1977,11 @@ function debounce(wait) {
1921
1977
 
1922
1978
  // src/file-manager.ts
1923
1979
  import { createSourceFile as createSourceFile2, ScriptTarget as ScriptTarget2 } from "typescript";
1980
+ var localFileIdentifier = /\.[a-z]+$/i;
1981
+ var relativeSpecifierPattern = /(from\s*['"])(\.\.?\/[^'"]*?)(['"])/g;
1982
+ function rewriteRelativeSpecifiers(code) {
1983
+ return code.replace(relativeSpecifierPattern, (_, before, path, after) => localFileIdentifier.test(path) ? before + path + after : `${before}${path}.js${after}`);
1984
+ }
1924
1985
  var FileManager = class {
1925
1986
  hasEmittedFiles = false;
1926
1987
  declarationFiles = /* @__PURE__ */ new Map();
@@ -1979,14 +2040,17 @@ var FileManager = class {
1979
2040
  */
1980
2041
  finalize() {
1981
2042
  this.processEmittedFiles();
1982
- const buildInfoWrite = this.pendingBuildInfo ? Files.write(this.pendingBuildInfo.path, this.pendingBuildInfo.text) : void 0;
2043
+ const tasks = [];
2044
+ if (this.pendingBuildInfo) {
2045
+ tasks.push(Files.write(this.pendingBuildInfo.path, this.pendingBuildInfo.text));
2046
+ }
1983
2047
  this.pendingBuildInfo = void 0;
1984
2048
  if (this.cache && this.hasEmittedFiles) {
1985
- this.pendingSave = Promise.all([buildInfoWrite, this.cache.save(this.declarationFiles)]).then(() => {
1986
- });
1987
- } else if (buildInfoWrite) {
1988
- this.pendingSave = buildInfoWrite;
2049
+ tasks.push(this.cache.save(this.declarationFiles));
1989
2050
  }
2051
+ this.pendingSave = Promise.all(tasks).then(() => {
2052
+ }, () => {
2053
+ });
1990
2054
  this.pendingSave?.catch(() => {
1991
2055
  });
1992
2056
  return this.cache === void 0 || this.hasEmittedFiles;
@@ -2018,7 +2082,7 @@ var FileManager = class {
2018
2082
  writeTasks.push(this.writeFile(projectDirectory, filePath, code));
2019
2083
  }
2020
2084
  }
2021
- return (await Promise.all(writeTasks)).filter((result) => result !== void 0);
2085
+ return Promise.all(writeTasks);
2022
2086
  }
2023
2087
  /**
2024
2088
  * Resolves entry points for declaration bundling.
@@ -2033,8 +2097,9 @@ var FileManager = class {
2033
2097
  return defaultEntryPoint in projectEntryPoints ? { [defaultEntryPoint]: projectEntryPoints[defaultEntryPoint] } : projectEntryPoints;
2034
2098
  }
2035
2099
  const result = {};
2100
+ const allowedEntryPoints = new Set(dtsEntryPoints);
2036
2101
  for (const [name, path] of Object.entries(projectEntryPoints)) {
2037
- if (dtsEntryPoints.includes(name)) {
2102
+ if (allowedEntryPoints.has(name)) {
2038
2103
  result[name] = path;
2039
2104
  }
2040
2105
  }
@@ -2298,7 +2363,12 @@ function inferEntryPoints(packageJson, outDir, sourceDir = "src") {
2298
2363
  }
2299
2364
  }
2300
2365
  }
2301
- if (Object.keys(entryPoints).length === 0) {
2366
+ let hasEntries = false;
2367
+ for (const _ in entryPoints) {
2368
+ hasEntries = true;
2369
+ break;
2370
+ }
2371
+ if (!hasEntries) {
2302
2372
  const legacyPath = packageJson.module ?? packageJson.main;
2303
2373
  if (legacyPath !== void 0) {
2304
2374
  const sourcePath = outputToSourcePath(legacyPath, outDir, sourceDir);
@@ -2307,11 +2377,13 @@ function inferEntryPoints(packageJson, outDir, sourceDir = "src") {
2307
2377
  }
2308
2378
  }
2309
2379
  }
2310
- return Object.keys(entryPoints).length > 0 ? entryPoints : void 0;
2380
+ for (const _ in entryPoints) {
2381
+ return entryPoints;
2382
+ }
2383
+ return void 0;
2311
2384
  }
2312
2385
 
2313
2386
  // src/type-script-project.ts
2314
- import { build as esbuild2, formatMessages } from "esbuild";
2315
2387
  import { performance as performance2 } from "node:perf_hooks";
2316
2388
  import { sys as sys2, createIncrementalProgram, formatDiagnostics, formatDiagnosticsWithColorAndContext, parseJsonConfigFileContent, readConfigFile, findConfigFile } from "typescript";
2317
2389
  var globCharacters = /[*?\\[\]!].*$/;
@@ -2355,7 +2427,7 @@ var _TypeScriptProject = class _TypeScriptProject {
2355
2427
  return Files.empty(this.buildConfiguration.outDir);
2356
2428
  }
2357
2429
  async build() {
2358
- Logger.header(`${tsLogo} tsbuild v${"1.8.2"}${this.configuration.compilerOptions.incremental && this.configuration.buildCache?.isValid() ? " [incremental]" : ""}`);
2430
+ Logger.header(`${tsLogo} tsbuild v${"1.8.4"}${this.configuration.compilerOptions.incremental && this.configuration.buildCache?.isValid() ? " [incremental]" : ""}`);
2359
2431
  try {
2360
2432
  const processes = [];
2361
2433
  const filesWereEmitted = await this.typeCheck();
@@ -2386,7 +2458,7 @@ var _TypeScriptProject = class _TypeScriptProject {
2386
2458
  }
2387
2459
  }
2388
2460
  if (this.fileWatcher === void 0 || this.fileWatcher.isClosed()) {
2389
- setImmediate(() => this.watch());
2461
+ setImmediate(() => void this.watch());
2390
2462
  }
2391
2463
  }
2392
2464
  }
@@ -2425,6 +2497,7 @@ var _TypeScriptProject = class _TypeScriptProject {
2425
2497
  return emitted || !this.configuration.compilerOptions.declaration;
2426
2498
  }
2427
2499
  async transpile() {
2500
+ const { build: esbuild, formatMessages } = await import("esbuild");
2428
2501
  const plugins = [outputPlugin()];
2429
2502
  if (this.buildConfiguration.noExternal.length > 0) {
2430
2503
  plugins.push(externalModulesPlugin({ dependencies: await this.getProjectDependencyPaths(), noExternal: this.buildConfiguration.noExternal }));
@@ -2453,11 +2526,11 @@ var _TypeScriptProject = class _TypeScriptProject {
2453
2526
  }
2454
2527
  }
2455
2528
  try {
2456
- const { warnings, errors, metafile: { outputs } } = await esbuild2({
2529
+ const { warnings, errors, metafile: { outputs } } = await esbuild({
2457
2530
  format,
2458
2531
  plugins,
2459
2532
  define,
2460
- write: false,
2533
+ write: true,
2461
2534
  metafile: true,
2462
2535
  treeShaking: true,
2463
2536
  logLevel: "warning",
@@ -2517,7 +2590,8 @@ var _TypeScriptProject = class _TypeScriptProject {
2517
2590
  /**
2518
2591
  * Watches for changes in the project files and rebuilds the project when changes are detected.
2519
2592
  */
2520
- watch() {
2593
+ async watch() {
2594
+ const { Watchr } = await import("@d1g1tal/watchr");
2521
2595
  const targets = [];
2522
2596
  for (const path of this.configuration.include ?? [defaultSourceDirectory]) {
2523
2597
  targets.push(Paths.absolute(this.directory, path.replace(globCharacters, "")));
@@ -2571,7 +2645,7 @@ var _TypeScriptProject = class _TypeScriptProject {
2571
2645
  Logger.info(`Rebuilding project: ${this.pendingChanges.length} file changes detected.`);
2572
2646
  const rootNames = [...this.builderProgram.getProgram().getRootFileNames()];
2573
2647
  for (const { event, path, nextPath } of this.pendingChanges) {
2574
- if (nextPath !== void 0 && (event === Watchr.FileEvent.rename || event === Watchr.DirectoryEvent.rename)) {
2648
+ if (nextPath !== void 0 && (event === "rename" || event === "renameDir")) {
2575
2649
  this.buildDependencies.delete(Paths.relative(this.directory, path));
2576
2650
  this.buildDependencies.add(Paths.relative(this.directory, nextPath));
2577
2651
  const index = rootNames.indexOf(path);
@@ -2580,9 +2654,9 @@ var _TypeScriptProject = class _TypeScriptProject {
2580
2654
  }
2581
2655
  } else {
2582
2656
  const index = rootNames.indexOf(path);
2583
- if (event === Watchr.FileEvent.unlink && index !== -1) {
2657
+ if (event === "unlink" && index !== -1) {
2584
2658
  rootNames.splice(index, 1);
2585
- } else if (event === Watchr.FileEvent.add && index === -1) {
2659
+ } else if (event === "add" && index === -1) {
2586
2660
  rootNames.push(path);
2587
2661
  }
2588
2662
  }
package/dist/tsbuild.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  BuildError,
4
4
  TypeScriptProject
5
- } from "./ALZQNZZK.js";
5
+ } from "./23A5VAYC.js";
6
6
  import "./JKGYA2AW.js";
7
7
 
8
8
  // src/tsbuild.ts
@@ -30,7 +30,7 @@ if (help) {
30
30
  process.exit(0);
31
31
  }
32
32
  if (version) {
33
- console.log("1.8.2");
33
+ console.log("1.8.4");
34
34
  process.exit(0);
35
35
  }
36
36
  var typeScriptOptions = {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TypeScriptProject
3
- } from "./ALZQNZZK.js";
3
+ } from "./23A5VAYC.js";
4
4
  import "./JKGYA2AW.js";
5
5
  export {
6
6
  TypeScriptProject
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@d1g1tal/tsbuild",
3
3
  "author": "D1g1talEntr0py",
4
- "version": "1.8.2",
4
+ "version": "1.8.4",
5
5
  "license": "MIT",
6
6
  "description": "A fast, ESM-only TypeScript build tool combining the TypeScript API for type checking and declaration generation, esbuild for bundling, and SWC for decorator metadata.",
7
7
  "homepage": "https://github.com/D1g1talEntr0py/tsbuild#readme",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "@eslint/js": "^10.0.1",
53
- "@types/node": "^25.5.2",
53
+ "@types/node": "^25.6.0",
54
54
  "@typescript-eslint/eslint-plugin": "^8.58.1",
55
55
  "@typescript-eslint/parser": "^8.58.1",
56
56
  "@vitest/coverage-v8": "^4.1.4",
@@ -81,6 +81,7 @@
81
81
  "scripts": {
82
82
  "build": "tsx ./src/tsbuild.ts",
83
83
  "build:watch": "tsx ./src/tsbuild.ts --watch",
84
+ "bench": "tsx ./scripts/benchmark.ts",
84
85
  "type-check": "tsx ./src/tsbuild.ts --noEmit",
85
86
  "lint": "eslint ./src",
86
87
  "test": "vitest run",