@reliverse/dler 1.7.73 → 1.7.74

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 (43) hide show
  1. package/README.md +2 -2
  2. package/bin/app/agg/cmd.d.ts +40 -0
  3. package/bin/app/agg/cmd.js +833 -27
  4. package/bin/app/build/postbuild.js +1 -0
  5. package/bin/app/cmds.d.ts +230 -4
  6. package/bin/app/cmds.js +36 -19
  7. package/bin/app/config/cmd.d.ts +18 -0
  8. package/bin/app/config/cmd.js +43 -0
  9. package/bin/app/config/impl/typebox.d.ts +8 -0
  10. package/bin/app/config/impl/typebox.js +82 -0
  11. package/bin/app/mkdist/cmd.js +1 -1
  12. package/bin/app/pub/cmd.js +1 -1
  13. package/bin/app/rempts/cmd.js +146 -8
  14. package/bin/cli.js +14 -20
  15. package/bin/libs/cfg/cfg-impl/{cfg-types.d.ts → cfg-dler.d.ts} +76 -0
  16. package/bin/libs/cfg/cfg-mod.d.ts +2 -2
  17. package/bin/libs/cfg/cfg-mod.js +1 -1
  18. package/bin/libs/get/get-impl/get-core.d.ts +3 -0
  19. package/bin/libs/get/get-impl/get-core.js +450 -0
  20. package/bin/libs/get/get-mod.d.ts +0 -6
  21. package/bin/libs/get/get-mod.js +38 -459
  22. package/bin/libs/sdk/sdk-impl/build/providers/bun/single-file.d.ts +2 -2
  23. package/bin/libs/sdk/sdk-impl/config/core.d.ts +8 -0
  24. package/bin/libs/sdk/sdk-impl/config/core.js +231 -0
  25. package/bin/libs/sdk/sdk-impl/config/info.js +1 -1
  26. package/bin/libs/sdk/sdk-impl/config/{init.js → prepare.js} +3 -1
  27. package/bin/libs/sdk/sdk-impl/magic/magic-spells.js +2 -1
  28. package/bin/libs/sdk/sdk-impl/utils/pm/pm-meta.d.ts +1 -1
  29. package/bin/libs/sdk/sdk-impl/utils/pm/pm-meta.js +2 -2
  30. package/bin/libs/sdk/sdk-impl/utils/resolve-cross-libs.d.ts +1 -3
  31. package/bin/libs/sdk/sdk-impl/utils/resolve-cross-libs.js +432 -9
  32. package/bin/libs/sdk/sdk-mod.d.ts +7 -4
  33. package/bin/libs/sdk/sdk-mod.js +18 -7
  34. package/bin/mod.js +4 -1
  35. package/package.json +5 -5
  36. package/bin/app/agg/impl.d.ts +0 -39
  37. package/bin/app/agg/impl.js +0 -341
  38. package/bin/app/agg/run.d.ts +0 -1
  39. package/bin/app/agg/run.js +0 -136
  40. package/bin/libs/cfg/cfg-impl/cfg-consts.d.ts +0 -77
  41. package/bin/libs/cfg/cfg-impl/cfg-types.js +0 -0
  42. /package/bin/libs/cfg/cfg-impl/{cfg-consts.js → cfg-dler.js} +0 -0
  43. /package/bin/libs/sdk/sdk-impl/config/{init.d.ts → prepare.d.ts} +0 -0
@@ -1,100 +1,120 @@
1
1
  import path from "@reliverse/pathkit";
2
- import { defineArgs, defineCommand, inputPrompt } from "@reliverse/rempts";
3
- import { useAggregator } from "./impl.js";
2
+ import fs from "@reliverse/relifso";
3
+ import { relinka } from "@reliverse/relinka";
4
+ import {
5
+ selectPrompt,
6
+ confirmPrompt,
7
+ inputPrompt,
8
+ defineArgs,
9
+ defineCommand,
10
+ } from "@reliverse/rempts";
11
+
12
+ import { getConfigDler } from "~/libs/sdk/sdk-impl/config/load";
13
+
14
+ const AGGREGATOR_START = "// AUTO-GENERATED AGGREGATOR START (via `dler agg`)";
15
+ const AGGREGATOR_END = "// AUTO-GENERATED AGGREGATOR END";
16
+
4
17
  export default defineCommand({
5
18
  args: defineArgs({
6
19
  imports: {
7
20
  description: "If true, produce import lines instead of export lines",
8
- type: "boolean"
21
+ type: "boolean",
9
22
  },
10
23
  input: {
11
24
  description: "Directory containing .ts/.js files (--input <directory>)",
12
- type: "string"
25
+ type: "string",
13
26
  },
14
27
  named: {
15
28
  description: "Parse each file for named exports (function/class/const/let)",
16
29
  type: "boolean",
17
- default: true
30
+ default: true,
18
31
  },
19
32
  out: {
20
33
  description: "Output aggregator file path (--out <fileName>)",
21
- type: "string"
34
+ type: "string",
22
35
  },
23
36
  recursive: {
24
- description: "Recursively scan subdirectories (default true) (false means only scan the files in the current directory and not subdirectories)",
37
+ description:
38
+ "Recursively scan subdirectories (default true) (false means only scan the files in the current directory and not subdirectories)",
25
39
  type: "boolean",
26
- default: true
40
+ default: true,
27
41
  },
28
42
  strip: {
29
43
  description: "Remove specified path prefix from final imports/exports",
30
- type: "string"
44
+ type: "string",
31
45
  },
32
46
  sort: {
33
47
  description: "Sort aggregated lines alphabetically",
34
- type: "boolean"
48
+ type: "boolean",
35
49
  },
36
50
  header: {
37
51
  description: "Add a header comment to the aggregator output",
38
- type: "string"
52
+ type: "string",
39
53
  },
40
54
  verbose: {
41
55
  description: "Enable verbose logging",
42
- type: "boolean"
56
+ type: "boolean",
43
57
  },
44
58
  includeInternal: {
45
59
  description: "Include files marked as internal (starting with #)",
46
- type: "boolean"
60
+ type: "boolean",
47
61
  },
48
62
  internalMarker: {
49
63
  description: "Marker for internal files (default: #)",
50
64
  type: "string",
51
- default: "#"
65
+ default: "#",
52
66
  },
53
67
  override: {
54
68
  description: "Override entire file instead of updating only the aggregator block",
55
- type: "boolean"
69
+ type: "boolean",
56
70
  },
57
71
  extensions: {
58
- description: "Comma-separated list of file extensions to process (default: .ts,.js,.mts,.cts,.mjs,.cjs)",
72
+ description:
73
+ "Comma-separated list of file extensions to process (default: .ts,.js,.mts,.cts,.mjs,.cjs)",
59
74
  type: "string",
60
- default: ".ts,.js,.mts,.cts,.mjs,.cjs"
75
+ default: ".ts,.js,.mts,.cts,.mjs,.cjs",
61
76
  },
62
77
  separateTypesFile: {
63
78
  description: "Create a separate file for type exports",
64
- type: "boolean"
79
+ type: "boolean",
65
80
  },
66
81
  typesOut: {
67
82
  description: "Output file path for types (used when separateTypesFile is true)",
68
- type: "string"
83
+ type: "string",
69
84
  },
70
85
  nonInteractive: {
71
86
  description: "Disable interactive prompts and require all arguments to be provided via flags",
72
87
  type: "boolean",
73
- default: false
74
- }
88
+ default: false,
89
+ },
75
90
  }),
76
91
  async run({ args }) {
77
92
  const resolvedArgs = { ...args };
93
+
94
+ // Handle required arguments with prompts when nonInteractive is false
78
95
  if (!args.nonInteractive) {
79
96
  if (!args.input) {
80
97
  resolvedArgs.input = await inputPrompt({
81
98
  title: "Enter input directory containing .ts/.js files:",
82
- defaultValue: ""
99
+ defaultValue: "",
83
100
  });
84
101
  }
102
+
85
103
  if (!args.out) {
86
104
  resolvedArgs.out = await inputPrompt({
87
105
  title: "Enter output aggregator file path:",
88
- defaultValue: ""
106
+ defaultValue: "",
89
107
  });
90
108
  }
109
+
91
110
  if (args.separateTypesFile && !args.typesOut) {
92
111
  resolvedArgs.typesOut = await inputPrompt({
93
112
  title: "Enter output file path for types:",
94
- defaultValue: resolvedArgs.out.replace(/\.(ts|js)$/, ".types.$1")
113
+ defaultValue: resolvedArgs.out.replace(/\.(ts|js)$/, ".types.$1"),
95
114
  });
96
115
  }
97
116
  } else {
117
+ // Validate required arguments in non-interactive mode
98
118
  if (!args.input) {
99
119
  throw new Error("Missing required argument: --input");
100
120
  }
@@ -103,10 +123,11 @@ export default defineCommand({
103
123
  }
104
124
  if (args.separateTypesFile && !args.typesOut) {
105
125
  throw new Error(
106
- "Missing required argument: --typesOut (required when --separateTypesFile is true)"
126
+ "Missing required argument: --typesOut (required when --separateTypesFile is true)",
107
127
  );
108
128
  }
109
129
  }
130
+
110
131
  await useAggregator({
111
132
  inputDir: path.resolve(resolvedArgs.input),
112
133
  isRecursive: !!resolvedArgs.recursive,
@@ -122,7 +143,792 @@ export default defineCommand({
122
143
  overrideFile: !!resolvedArgs.override,
123
144
  fileExtensions: resolvedArgs.extensions.split(",").map((ext) => ext.trim()),
124
145
  separateTypesFile: !!resolvedArgs.separateTypesFile,
125
- typesOutFile: resolvedArgs.typesOut ? path.resolve(resolvedArgs.typesOut) : void 0
146
+ typesOutFile: resolvedArgs.typesOut ? path.resolve(resolvedArgs.typesOut) : undefined,
126
147
  });
127
- }
148
+ },
128
149
  });
150
+
151
+ /**
152
+ * Checks if a file exists at the given path
153
+ */
154
+ async function fileExists(filePath: string): Promise<boolean> {
155
+ return await fs.pathExists(filePath);
156
+ }
157
+
158
+ /**
159
+ * Checks if the first line of a file contains the disable aggregation comment
160
+ */
161
+ async function isAggregationDisabled(filePath: string): Promise<boolean> {
162
+ try {
163
+ const content = await fs.readFile(filePath, "utf-8");
164
+ const firstLine = content.split("\n")[0]?.trim();
165
+ return firstLine === "// <dler-disable-agg>";
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Finds the main package based on dler configuration with fallbacks
173
+ */
174
+ async function findMainEntryFile(config: any): Promise<string | null> {
175
+ const { coreEntryFile, coreEntrySrcDir } = config;
176
+
177
+ // Check the configured entry file first
178
+ if (coreEntryFile && coreEntrySrcDir) {
179
+ const configuredPath = path.join(coreEntrySrcDir, coreEntryFile);
180
+ if (await fileExists(configuredPath)) {
181
+ return configuredPath;
182
+ }
183
+ }
184
+
185
+ // Fallback to common entry file patterns
186
+ const fallbackPatterns = [
187
+ path.join(coreEntrySrcDir || "src", "mod.ts"),
188
+ path.join(coreEntrySrcDir || "src", "index.ts"),
189
+ path.join(coreEntrySrcDir || "src", "mod.js"),
190
+ path.join(coreEntrySrcDir || "src", "index.js"),
191
+ ];
192
+
193
+ for (const pattern of fallbackPatterns) {
194
+ if (await fileExists(pattern)) {
195
+ return pattern;
196
+ }
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ export async function promptAggCommand() {
203
+ // Try to load config and check for libs
204
+ const config = await getConfigDler();
205
+ let selectedLibName: string | null = null;
206
+
207
+ // Check for main package
208
+ const mainEntryFile = await findMainEntryFile(config);
209
+ const isMainDisabled = mainEntryFile ? await isAggregationDisabled(mainEntryFile) : false;
210
+
211
+ if (config?.libsList && Object.keys(config.libsList).length > 0) {
212
+ const libEntries = await Promise.all(
213
+ Object.entries(config.libsList).map(async ([name, lib]) => {
214
+ const libMainFile = `${config.libsDirSrc}/${lib.libMainFile}`;
215
+ const isLibDisabled = await isAggregationDisabled(libMainFile);
216
+
217
+ return {
218
+ name,
219
+ lib,
220
+ isDisabled: isLibDisabled,
221
+ };
222
+ }),
223
+ );
224
+
225
+ const libs = libEntries
226
+ .filter(({ isDisabled }) => !isDisabled)
227
+ .map(({ name, lib }) => ({
228
+ value: name,
229
+ label: name,
230
+ hint: `${config.libsDirSrc}/${lib.libDirName}/${lib.libDirName}-impl`,
231
+ }));
232
+
233
+ // Add main package option if found and not disabled
234
+ if (mainEntryFile && !isMainDisabled) {
235
+ libs.unshift({
236
+ value: "main",
237
+ label: "Main package",
238
+ hint: mainEntryFile,
239
+ });
240
+ }
241
+
242
+ // Add "Skip" option
243
+ libs.push({ value: "", label: "Skip selection", hint: "" });
244
+
245
+ selectedLibName = await selectPrompt({
246
+ title: "Select a package to aggregate or skip",
247
+ options: libs,
248
+ });
249
+ } else if (mainEntryFile && !isMainDisabled) {
250
+ // If no libs but main package exists and is not disabled, offer it as the only option
251
+ const shouldUseMain = await confirmPrompt({
252
+ title: `Use main package for aggregation? (Found: ${mainEntryFile})`,
253
+ defaultValue: true,
254
+ });
255
+
256
+ if (shouldUseMain) {
257
+ selectedLibName = "main";
258
+ }
259
+ }
260
+
261
+ // If lib selected, use its config
262
+ let imports = false;
263
+ let input = "";
264
+ let named = true;
265
+ let out = "";
266
+ let recursive = true;
267
+ let strip = "";
268
+ let separateTypesFile = false;
269
+ let typesOut = "";
270
+
271
+ if (selectedLibName && selectedLibName !== "") {
272
+ if (selectedLibName === "main" && mainEntryFile && !isMainDisabled) {
273
+ // Use main package configuration
274
+ const entryDir = path.dirname(mainEntryFile);
275
+
276
+ input = entryDir;
277
+ out = mainEntryFile;
278
+ strip = entryDir;
279
+ } else if (selectedLibName === "main" && isMainDisabled) {
280
+ // Main package is disabled, exit early
281
+ relinka.log("Main package aggregation is disabled due to <dler-disable-agg> comment.");
282
+ return;
283
+ } else {
284
+ // Use library configuration
285
+ const libConfig = config?.libsList?.[selectedLibName];
286
+ if (config && libConfig) {
287
+ input = `${config.libsDirSrc}/${libConfig.libDirName}/${libConfig.libDirName}-impl`;
288
+ out = `${config.libsDirSrc}/${libConfig.libMainFile}`;
289
+ strip = `${config.libsDirSrc}/${libConfig.libDirName}`;
290
+ }
291
+ }
292
+ }
293
+
294
+ // Only prompt for values not set by lib config
295
+ if (!selectedLibName || !input) {
296
+ input = await inputPrompt({
297
+ title: "Enter the input directory",
298
+ defaultValue: input,
299
+ });
300
+
301
+ // Check if manually entered input corresponds to a disabled file
302
+ if (input) {
303
+ // Check if the input is pointing to a disabled main file (directory or file)
304
+ if (mainEntryFile && isMainDisabled) {
305
+ const mainEntryDir = path.dirname(mainEntryFile);
306
+ if (
307
+ path.resolve(input) === path.resolve(mainEntryDir) ||
308
+ path.resolve(input) === path.resolve(mainEntryFile)
309
+ ) {
310
+ relinka.log("Main package aggregation is disabled due to <dler-disable-agg> comment.");
311
+ return;
312
+ }
313
+ }
314
+
315
+ // Check if the input is pointing to a disabled library
316
+ if (config?.libsList) {
317
+ for (const [libName, libConfig] of Object.entries(config.libsList)) {
318
+ const libImplPath = `${config.libsDirSrc}/${libConfig.libDirName}/${libConfig.libDirName}-impl`;
319
+ const libMainFile = `${config.libsDirSrc}/${libConfig.libMainFile}`;
320
+
321
+ if (
322
+ path.resolve(input) === path.resolve(libImplPath) ||
323
+ path.resolve(input) === path.resolve(libMainFile)
324
+ ) {
325
+ const isLibDisabled = await isAggregationDisabled(libMainFile);
326
+ if (isLibDisabled) {
327
+ relinka.log(
328
+ `Library "${libName}" aggregation is disabled due to <dler-disable-agg> comment.`,
329
+ );
330
+ return;
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ // Ask for verbose mode first to determine if we should show additional options
339
+ const verbose = await confirmPrompt({
340
+ title: "Enable verbose logging and additional options?",
341
+ defaultValue: false,
342
+ });
343
+
344
+ // Default values for non-essential options
345
+ let sortLines = false;
346
+ let headerComment = "";
347
+ let includeInternal = false;
348
+ let internalMarker = "#";
349
+ let overrideFile = false;
350
+ let extensions = ".ts,.js,.mts,.cts,.mjs,.cjs";
351
+
352
+ // Only ask non-essential questions if verbose mode is enabled
353
+ if (verbose) {
354
+ sortLines = await confirmPrompt({
355
+ title: "Sort aggregated lines alphabetically?",
356
+ defaultValue: false,
357
+ });
358
+
359
+ headerComment = await inputPrompt({
360
+ title: "Add a header comment to the aggregator output (optional):",
361
+ defaultValue: "",
362
+ });
363
+
364
+ includeInternal = await confirmPrompt({
365
+ title: "Include files marked as internal (starting with #)?",
366
+ defaultValue: false,
367
+ });
368
+
369
+ internalMarker = await inputPrompt({
370
+ title: "Marker for internal files:",
371
+ defaultValue: "#",
372
+ });
373
+
374
+ overrideFile = await confirmPrompt({
375
+ title: "Override entire file instead of updating only the aggregator block?",
376
+ defaultValue: false,
377
+ });
378
+
379
+ extensions = await inputPrompt({
380
+ title: "File extensions to process (comma-separated):",
381
+ defaultValue: ".ts,.js,.mts,.cts,.mjs,.cjs",
382
+ });
383
+
384
+ imports = await confirmPrompt({
385
+ title: "Do you want to generate imports instead of exports? (N generates exports)",
386
+ defaultValue: imports,
387
+ });
388
+
389
+ named = await confirmPrompt({
390
+ title: imports
391
+ ? "Do you want to generate named imports?"
392
+ : "Do you want to generate named exports?",
393
+ defaultValue: named,
394
+ });
395
+
396
+ recursive = await confirmPrompt({
397
+ title: "Do you want to recursively scan subdirectories?",
398
+ defaultValue: recursive,
399
+ });
400
+
401
+ separateTypesFile = await confirmPrompt({
402
+ title: "Do you want to create a separate file for type exports?",
403
+ defaultValue: separateTypesFile,
404
+ });
405
+ }
406
+
407
+ if (!selectedLibName || !out) {
408
+ out = await inputPrompt({
409
+ title: "Enter the output file",
410
+ defaultValue: out,
411
+ });
412
+ }
413
+
414
+ if (!selectedLibName || !strip) {
415
+ strip = await inputPrompt({
416
+ title: "Enter the path to strip from the final imports/exports",
417
+ defaultValue: strip,
418
+ });
419
+ }
420
+
421
+ if (separateTypesFile) {
422
+ typesOut = await inputPrompt({
423
+ title: "Enter the output file for types",
424
+ defaultValue: out.replace(/\.(ts|js)$/, ".types.$1"),
425
+ });
426
+ }
427
+
428
+ await useAggregator({
429
+ inputDir: path.resolve(input),
430
+ isRecursive: recursive,
431
+ outFile: path.resolve(out),
432
+ stripPrefix: strip ? path.resolve(strip) : "",
433
+ useImport: imports,
434
+ useNamed: named,
435
+ sortLines: sortLines,
436
+ headerComment: headerComment,
437
+ verbose: verbose,
438
+ includeInternal: includeInternal,
439
+ internalMarker: internalMarker,
440
+ overrideFile: overrideFile,
441
+ fileExtensions: extensions.split(",").map((ext) => ext.trim()),
442
+ separateTypesFile: separateTypesFile,
443
+ typesOutFile: typesOut ? path.resolve(typesOut) : undefined,
444
+ });
445
+ }
446
+
447
+ /**
448
+ * Aggregator supporting:
449
+ * - --import or default export,
450
+ * - star or named exports,
451
+ * - separate "type" vs "value" for both import and export.
452
+ *
453
+ * Options:
454
+ * - Option to ignore specific directories (default: node_modules, .git)
455
+ * - Option to sort aggregated lines alphabetically.
456
+ * - Option to add a header comment in the aggregator output.
457
+ * - Option to enable verbose logging.
458
+ * - Deduplicates overloaded export names.
459
+ * - Skips files whose basenames start with an internal marker (default: "#")
460
+ * unless includeInternal is true or an alternative marker is provided.
461
+ * - By default, updates only the auto-generated block in the aggregator file,
462
+ * leaving any other content intact. Pass `overrideFile: true` to rewrite the entire file.
463
+ */
464
+ export async function useAggregator({
465
+ inputDir,
466
+ isRecursive,
467
+ outFile,
468
+ stripPrefix,
469
+ useImport,
470
+ useNamed,
471
+ ignoreDirs = ["node_modules", ".git"],
472
+ sortLines = false,
473
+ headerComment = "",
474
+ verbose = false,
475
+ includeInternal = false,
476
+ internalMarker = "#",
477
+ overrideFile = false,
478
+ fileExtensions = [".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"],
479
+ separateTypesFile = false,
480
+ typesOutFile,
481
+ }: {
482
+ inputDir: string;
483
+ isRecursive: boolean;
484
+ outFile: string;
485
+ stripPrefix: string;
486
+ useImport: boolean;
487
+ useNamed: boolean;
488
+ ignoreDirs?: string[];
489
+ sortLines?: boolean;
490
+ headerComment?: string;
491
+ verbose?: boolean;
492
+ includeInternal?: boolean;
493
+ internalMarker?: string;
494
+ overrideFile?: boolean;
495
+ fileExtensions?: string[];
496
+ separateTypesFile?: boolean;
497
+ typesOutFile?: string;
498
+ }) {
499
+ try {
500
+ // Validate input directory
501
+ const st = await fs.stat(inputDir).catch(() => null);
502
+ if (!st?.isDirectory()) {
503
+ relinka("error", `Error: --input is not a valid directory: ${inputDir}`);
504
+ process.exit(1);
505
+ }
506
+
507
+ // Validate output file directory exists or can be created
508
+ const outDir = path.dirname(outFile);
509
+ try {
510
+ await fs.ensureDir(outDir);
511
+ } catch (error) {
512
+ relinka("error", `Error: Cannot create output directory: ${outDir}\n${error}`);
513
+ process.exit(1);
514
+ }
515
+
516
+ // Validate types output file directory if separateTypesFile is true
517
+ if (separateTypesFile && typesOutFile) {
518
+ const typesOutDir = path.dirname(typesOutFile);
519
+ try {
520
+ await fs.ensureDir(typesOutDir);
521
+ } catch (error) {
522
+ relinka("error", `Error: Cannot create types output directory: ${typesOutDir}\n${error}`);
523
+ process.exit(1);
524
+ }
525
+ }
526
+
527
+ // Validate output file extension matches input extensions
528
+ const outExt = path.extname(outFile).toLowerCase();
529
+ if (!fileExtensions.includes(outExt)) {
530
+ relinka(
531
+ "warn",
532
+ `Warning: Output file extension (${outExt}) doesn't match any of the input extensions: ${fileExtensions.join(", ")}`,
533
+ );
534
+ }
535
+
536
+ // Validate strip prefix is a valid directory if provided
537
+ if (stripPrefix) {
538
+ const stripSt = await fs.stat(stripPrefix).catch(() => null);
539
+ if (!stripSt?.isDirectory()) {
540
+ relinka("error", `Error: --strip is not a valid directory: ${stripPrefix}`);
541
+ process.exit(1);
542
+ }
543
+ }
544
+
545
+ // Collect files with specified extensions
546
+ if (verbose)
547
+ relinka(
548
+ "log",
549
+ `Scanning directory ${inputDir} for files with extensions: ${fileExtensions.join(", ")}`,
550
+ );
551
+ const filePaths = await collectFiles(
552
+ inputDir,
553
+ fileExtensions,
554
+ isRecursive,
555
+ ignoreDirs,
556
+ verbose,
557
+ includeInternal,
558
+ internalMarker,
559
+ outFile,
560
+ );
561
+ if (!filePaths.length) {
562
+ relinka(
563
+ "warn",
564
+ `No matching files found in ${inputDir} with extensions: ${fileExtensions.join(", ")}`,
565
+ );
566
+ if (!overrideFile) {
567
+ relinka("warn", "No changes will be made to the output file.");
568
+ return;
569
+ }
570
+ }
571
+
572
+ // Generate aggregator lines concurrently with unique star-import identifiers
573
+ const usedIdentifiers = new Set<string>();
574
+ const aggregatorLinesArrays = await Promise.all(
575
+ filePaths.map((fp) =>
576
+ generateAggregatorLines(
577
+ fp,
578
+ inputDir,
579
+ stripPrefix,
580
+ useImport,
581
+ useNamed,
582
+ usedIdentifiers,
583
+ ).catch((error) => {
584
+ relinka("error", `Error processing file ${fp}: ${error}`);
585
+ return [];
586
+ }),
587
+ ),
588
+ );
589
+
590
+ // Separate type and value lines
591
+ const allLines = aggregatorLinesArrays.flat();
592
+ const typeLines: string[] = [];
593
+ const valueLines: string[] = [];
594
+
595
+ for (const line of allLines) {
596
+ if (line.includes("type {")) {
597
+ typeLines.push(line);
598
+ } else {
599
+ valueLines.push(line);
600
+ }
601
+ }
602
+
603
+ // Optionally sort lines alphabetically
604
+ if (sortLines) {
605
+ typeLines.sort();
606
+ valueLines.sort();
607
+ if (verbose) relinka("log", "Sorted aggregator lines alphabetically.");
608
+ }
609
+
610
+ // Build the aggregator block content
611
+ const buildAggregatorBlock = (lines: string[]) =>
612
+ `${headerComment ? `${headerComment}\n` : ""}${AGGREGATOR_START}\n${lines.join("\n")}\n${AGGREGATOR_END}\n`;
613
+
614
+ if (separateTypesFile && typesOutFile) {
615
+ // Write type exports to separate file
616
+ const typeBlock = buildAggregatorBlock(typeLines);
617
+ await fs.ensureFile(typesOutFile);
618
+ await fs.writeFile(typesOutFile, typeBlock, "utf8");
619
+
620
+ // Write value exports to main file, including type file import
621
+ const valueBlock = buildAggregatorBlock([
622
+ ...valueLines,
623
+ `export * from "${path.relative(path.dirname(outFile), typesOutFile).replace(/\\/g, "/")}";`,
624
+ ]);
625
+ await fs.ensureFile(outFile);
626
+ await fs.writeFile(outFile, valueBlock, "utf8");
627
+
628
+ relinka(
629
+ "success",
630
+ `Aggregator done: processed ${typeLines.length} type lines in: ${typesOutFile} and ${valueLines.length} value lines in: ${outFile}`,
631
+ );
632
+ } else {
633
+ // Write all lines to single file
634
+ const aggregatorBlock = buildAggregatorBlock(allLines);
635
+ await fs.ensureFile(outFile);
636
+ await fs.writeFile(outFile, aggregatorBlock, "utf8");
637
+
638
+ relinka("success", `Aggregator done: processed ${allLines.length} lines in: ${outFile}`);
639
+ }
640
+ } catch (error) {
641
+ relinka("error", `Aggregator failed: ${error}`);
642
+ process.exit(1);
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Build a relative import/export path, removing `stripPrefix` if it is truly a prefix,
648
+ * converting .ts -> .js, and ensuring it starts with "./" or "../".
649
+ */
650
+ function buildPathRelative(filePath: string, inputDir: string, stripPrefix: string): string {
651
+ let resolved = path.resolve(filePath);
652
+ const resolvedStrip = stripPrefix ? path.resolve(stripPrefix) : "";
653
+
654
+ // If stripPrefix applies, remove it; otherwise, compute a relative path.
655
+ if (resolvedStrip && resolved.startsWith(resolvedStrip)) {
656
+ resolved = resolved.slice(resolvedStrip.length);
657
+ } else {
658
+ resolved = path.relative(path.resolve(inputDir), resolved);
659
+ }
660
+
661
+ // Remove any leading path separator(s)
662
+ while (resolved.startsWith(path.sep)) {
663
+ resolved = resolved.slice(1);
664
+ }
665
+
666
+ // Normalize backslashes to forward slashes
667
+ resolved = resolved.replace(/\\/g, "/");
668
+
669
+ // Convert .ts -> .js extension
670
+ if (resolved.toLowerCase().endsWith(".ts")) {
671
+ resolved = `${resolved.slice(0, -3)}.js`;
672
+ }
673
+
674
+ // Ensure the path starts with "./" or "../" only if it doesn't already
675
+ if (!resolved.startsWith("./") && !resolved.startsWith("../")) {
676
+ resolved = `./${resolved}`;
677
+ }
678
+
679
+ // Fix any double slashes in the path
680
+ resolved = resolved.replace(/\/+/g, "/");
681
+
682
+ return resolved;
683
+ }
684
+
685
+ /**
686
+ * Recursively collects files with given extensions, ignoring specified directories
687
+ * and files marked as internal.
688
+ */
689
+ async function collectFiles(
690
+ dir: string,
691
+ exts: string[],
692
+ recursive: boolean,
693
+ ignoreDirs: string[],
694
+ verbose: boolean,
695
+ includeInternal: boolean,
696
+ internalMarker: string,
697
+ outFile?: string,
698
+ ): Promise<string[]> {
699
+ const found: string[] = [];
700
+ const entries = await fs.readdir(dir, { withFileTypes: true });
701
+
702
+ for (const entry of entries) {
703
+ const fullPath = path.join(dir, entry.name);
704
+
705
+ // Skip the output file if it matches
706
+ if (outFile && path.resolve(fullPath) === path.resolve(outFile)) {
707
+ if (verbose) {
708
+ relinka("log", `Skipping output file: ${fullPath}`);
709
+ }
710
+ continue;
711
+ }
712
+
713
+ if (entry.isDirectory()) {
714
+ if (ignoreDirs.includes(entry.name)) {
715
+ if (verbose) {
716
+ relinka("log", `Skipping ignored directory: ${fullPath}`);
717
+ }
718
+ continue;
719
+ }
720
+ if (recursive) {
721
+ const sub = await collectFiles(
722
+ fullPath,
723
+ exts,
724
+ recursive,
725
+ ignoreDirs,
726
+ verbose,
727
+ includeInternal,
728
+ internalMarker,
729
+ outFile,
730
+ );
731
+ found.push(...sub);
732
+ }
733
+ } else if (entry.isFile()) {
734
+ // Skip file if its basename starts with the internal marker and internal files are not included.
735
+ if (!includeInternal && path.basename(fullPath).startsWith(internalMarker)) {
736
+ if (verbose) {
737
+ relinka("log", `Skipping internal file: ${fullPath}`);
738
+ }
739
+ continue;
740
+ }
741
+ if (exts.some((ext) => entry.name.toLowerCase().endsWith(ext))) {
742
+ if (verbose) {
743
+ relinka("log", `Found file: ${fullPath}`);
744
+ }
745
+ found.push(fullPath);
746
+ }
747
+ }
748
+ }
749
+ return found;
750
+ }
751
+
752
+ /**
753
+ * Creates aggregator lines for a single file.
754
+ *
755
+ * If `useNamed` is true, parses named exports and produces up to two lines:
756
+ * - import type { ... } / export type { ... }
757
+ * - import { ... } / export { ... }
758
+ *
759
+ * If `useNamed` is false, produces a single star import or export.
760
+ *
761
+ * @param usedIdentifiers A set to track and ensure unique identifiers for star imports.
762
+ */
763
+ async function generateAggregatorLines(
764
+ filePath: string,
765
+ inputDir: string,
766
+ stripPrefix: string,
767
+ useImport: boolean,
768
+ useNamed: boolean,
769
+ usedIdentifiers?: Set<string>,
770
+ ): Promise<string[]> {
771
+ const importPath = buildPathRelative(filePath, inputDir, stripPrefix);
772
+
773
+ // Star import/export approach when not using named exports
774
+ if (!useNamed) {
775
+ if (useImport) {
776
+ let ident = guessStarImportIdentifier(filePath);
777
+ if (usedIdentifiers) {
778
+ let uniqueIdent = ident;
779
+ let counter = 1;
780
+ while (usedIdentifiers.has(uniqueIdent)) {
781
+ uniqueIdent = `${ident}_${counter}`;
782
+ counter++;
783
+ }
784
+ usedIdentifiers.add(uniqueIdent);
785
+ ident = uniqueIdent;
786
+ }
787
+ return [`import * as ${ident} from "${importPath}";`];
788
+ }
789
+ return [`export * from "${importPath}";`];
790
+ }
791
+
792
+ // For named exports, parse the file to extract export names.
793
+ const { typeNames, valueNames } = await getNamedExports(filePath);
794
+ if (!typeNames.length && !valueNames.length) {
795
+ return [];
796
+ }
797
+
798
+ if (useImport) {
799
+ const lines: string[] = [];
800
+ if (typeNames.length > 0) {
801
+ lines.push(`import type { ${typeNames.join(", ")} } from "${importPath}";`);
802
+ }
803
+ if (valueNames.length > 0) {
804
+ lines.push(`import { ${valueNames.join(", ")} } from "${importPath}";`);
805
+ }
806
+ return lines;
807
+ }
808
+
809
+ // For exports
810
+ const lines: string[] = [];
811
+ if (typeNames.length > 0) {
812
+ lines.push(`export type { ${typeNames.join(", ")} } from "${importPath}";`);
813
+ }
814
+ if (valueNames.length > 0) {
815
+ lines.push(`export { ${valueNames.join(", ")} } from "${importPath}";`);
816
+ }
817
+ return lines;
818
+ }
819
+
820
+ /**
821
+ * Parses a file to extract named exports, separating type exports from value exports.
822
+ * Deduplicates export names (to handle overloads).
823
+ */
824
+ async function getNamedExports(
825
+ filePath: string,
826
+ ): Promise<{ typeNames: string[]; valueNames: string[] }> {
827
+ try {
828
+ const code = await fs.readFile(filePath, "utf8");
829
+ const typeNamesSet = new Set<string>();
830
+ const valueNamesSet = new Set<string>();
831
+
832
+ // Match various export patterns:
833
+ // 1. Regular exports: export const/let/var/function/class/interface/type/enum
834
+ // 2. Default exports: export default class/function/const/interface
835
+ // 3. Named exports: export { name, name2 as alias }
836
+ // 4. Re-exports: export { name } from './other'
837
+ // 5. Export assignments: export = name
838
+ const patterns = [
839
+ // Regular exports and default exports
840
+ /^export\s+(?:default\s+)?(?:async\s+)?(function|const|class|let|var|type|interface|enum)\s+([A-Za-z0-9_$]+)/gm,
841
+ // Named exports and re-exports
842
+ /^export\s*{([^}]+)}(?:\s+from\s+['"][^'"]+['"])?/gm,
843
+ // Export assignments
844
+ /^export\s*=\s*([A-Za-z0-9_$]+)/gm,
845
+ ];
846
+
847
+ for (const pattern of patterns) {
848
+ let match: RegExpExecArray | null;
849
+ while (true) {
850
+ match = pattern.exec(code);
851
+ if (!match) break;
852
+
853
+ const matchGroups = match as RegExpExecArray & Record<number, string>;
854
+ if (pattern.source.includes("{([^}]+)}") && matchGroups[1]) {
855
+ // Handle named exports/re-exports
856
+ const exports = (matchGroups[1] ?? "").split(",").map(
857
+ (e) =>
858
+ e
859
+ ?.trim()
860
+ ?.split(/\s+as\s+/)?.[0]
861
+ ?.trim() ?? "",
862
+ );
863
+ for (const exp of exports) {
864
+ // Skip 'type' keyword in named exports
865
+ const name = exp.replace(/^type\s+/, "");
866
+ if (exp.startsWith("type ")) {
867
+ typeNamesSet.add(name);
868
+ } else {
869
+ valueNamesSet.add(name);
870
+ }
871
+ }
872
+ } else if (pattern.source.includes("=\\s*([A-Za-z0-9_$]+)") && matchGroups[1]) {
873
+ // Handle export assignments
874
+ valueNamesSet.add(matchGroups[1]);
875
+ } else {
876
+ // Handle regular exports
877
+ const keyword = matchGroups[1];
878
+ const name = matchGroups[2];
879
+ if (keyword && name) {
880
+ if (keyword === "type" || keyword === "interface" || keyword === "enum") {
881
+ typeNamesSet.add(name);
882
+ } else {
883
+ valueNamesSet.add(name);
884
+ }
885
+ }
886
+ }
887
+ }
888
+ }
889
+
890
+ return {
891
+ typeNames: Array.from(typeNamesSet),
892
+ valueNames: Array.from(valueNamesSet),
893
+ };
894
+ } catch (error) {
895
+ relinka("error", `Error reading file ${filePath}: ${error}`);
896
+ return { typeNames: [], valueNames: [] };
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Generates a valid identifier for star imports based on the file name.
902
+ */
903
+ function guessStarImportIdentifier(filePath: string): string {
904
+ const base = path.basename(filePath, path.extname(filePath));
905
+ let identifier = base.replace(/[^a-zA-Z0-9_$]/g, "_");
906
+ if (/^\d/.test(identifier)) {
907
+ identifier = `_${identifier}`;
908
+ }
909
+ return identifier || "file";
910
+ }
911
+
912
+ /**
913
+ * Prints usage examples based on whether dev mode or not.
914
+ */
915
+ export function printUsage(isDev?: boolean) {
916
+ relinka("log", "====================");
917
+ relinka("log", "TOOLS USAGE EXAMPLES");
918
+ relinka("log", "====================");
919
+ relinka(
920
+ "log",
921
+ `${isDev ? "bun dev:agg" : "dler tools"} --tool agg --input <dir> --out <file> [options]`,
922
+ );
923
+ if (isDev) {
924
+ relinka(
925
+ "log",
926
+ "bun dev:tools agg --input src/libs/sdk/sdk-impl --out src/libs/sdk/sdk-mod.ts --recursive --named --strip src/libs/sdk",
927
+ );
928
+ } else {
929
+ relinka(
930
+ "log",
931
+ "dler tools --tool agg --input src/libs --out aggregator.ts --recursive --named",
932
+ );
933
+ }
934
+ }