@adonisjs/assembler 8.0.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,9 +1,8 @@
1
- import { a as loadHooks, c as readTsConfig, d as runNode, f as throttle, m as debug_default, n as copyFiles, o as memoize, p as watch, r as getPort, s as parseConfig, t as VirtualFileSystem, u as run } from "./virtual_file_system-bGeoWsK-.js";
2
- import { n as FileBuffer } from "./source-dVeugJ0e.js";
3
- import { IndexGenerator } from "./src/index_generator/main.js";
4
- import "./validator_extractor-Ccio_Ndi.js";
5
- import { RoutesScanner } from "./src/code_scanners/routes_scanner/main.js";
6
- import { t as CodemodException } from "./codemod_exception-CzQgXAAf.js";
1
+ import "./chunk-DF48asd8.js";
2
+ import { a as loadHooks, c as readTsConfig, d as runNode, f as throttle, m as debug_default, n as copyFiles, o as memoize, p as watch, r as getPort, s as parseConfig, t as VirtualFileSystem, u as run } from "./virtual_file_system-dzfXNwEp.js";
3
+ import { n as FileBuffer, t as IndexGenerator } from "./main-INOi9swJ.js";
4
+ import { t as RoutesScanner } from "./main-Cpfvmdw6.js";
5
+ import { t as CodemodException } from "./codemod_exception-BMNJZ0i1.js";
7
6
  import dedent from "dedent";
8
7
  import fs, { readFile, unlink } from "node:fs/promises";
9
8
  import { cliui } from "@poppinss/cliui";
@@ -15,6 +14,11 @@ import getRandomPort from "get-port";
15
14
  import picomatch from "picomatch";
16
15
  import prettyHrtime from "pretty-hrtime";
17
16
  import { RuntimeException } from "@poppinss/utils/exception";
17
+ //#region src/bundler.ts
18
+ /**
19
+ * List of package managers we support in order to
20
+ * copy lockfiles
21
+ */
18
22
  const SUPPORTED_PACKAGE_MANAGERS = {
19
23
  "npm": {
20
24
  packageManagerFiles: ["package-lock.json"],
@@ -41,23 +45,65 @@ const SUPPORTED_PACKAGE_MANAGERS = {
41
45
  installCommand: "bun install --production"
42
46
  }
43
47
  };
48
+ /**
49
+ * The bundler class exposes the API to build an AdonisJS project.
50
+ *
51
+ * @example
52
+ * const bundler = new Bundler(new URL('./'), ts, { hooks: [] })
53
+ * const success = await bundler.bundle()
54
+ */
44
55
  var Bundler = class {
56
+ /**
57
+ * Reference to the TypeScript module
58
+ */
45
59
  #ts;
60
+ /**
61
+ * Hooks to execute custom actions during the build process
62
+ */
46
63
  #hooks;
64
+ /**
65
+ * Index generator for managing auto-generated index files
66
+ */
47
67
  #indexGenerator;
68
+ /**
69
+ * CLI UI instance for displaying colorful messages and progress information
70
+ */
48
71
  ui = cliui();
72
+ /**
73
+ * The current working directory URL
74
+ */
49
75
  cwd;
76
+ /**
77
+ * The current working project directory path as string
78
+ */
50
79
  cwdPath;
80
+ /**
81
+ * Bundler configuration options including hooks and meta files
82
+ */
51
83
  options;
84
+ /**
85
+ * Create a new bundler instance
86
+ *
87
+ * @param cwd - The current working directory URL
88
+ * @param ts - TypeScript module reference
89
+ * @param options - Bundler configuration options
90
+ */
52
91
  constructor(cwd, ts, options) {
53
92
  this.cwd = cwd;
54
93
  this.options = options;
55
94
  this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
56
95
  this.#ts = ts;
57
96
  }
97
+ /**
98
+ * Returns the relative unix path for an absolute
99
+ * file path
100
+ */
58
101
  #getRelativeName(filePath) {
59
102
  return string.toUnixSlash(relative(this.cwdPath, filePath));
60
103
  }
104
+ /**
105
+ * Cleans up the build directory
106
+ */
61
107
  async #cleanupBuildDirectory(outDir) {
62
108
  await fs.rm(outDir, {
63
109
  recursive: true,
@@ -65,6 +111,9 @@ var Bundler = class {
65
111
  maxRetries: 5
66
112
  });
67
113
  }
114
+ /**
115
+ * Runs tsc command to build the source.
116
+ */
68
117
  async #runTsc(outDir) {
69
118
  try {
70
119
  await run(this.cwd, {
@@ -77,15 +126,26 @@ var Bundler = class {
77
126
  return false;
78
127
  }
79
128
  }
129
+ /**
130
+ * Copy meta files to the output directory
131
+ */
80
132
  async #copyMetaFiles(outDir, additionalFilesToCopy) {
81
133
  await copyFiles((this.options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy), this.cwdPath, outDir);
82
134
  }
135
+ /**
136
+ * Detect the package manager used by the project
137
+ */
83
138
  async #detectPackageManager() {
84
139
  const pkgManager = await detectPackageManager(this.cwdPath);
85
140
  if (pkgManager === "deno") return "npm";
86
141
  if (pkgManager === "pnpm@6") return "pnpm";
87
142
  return pkgManager;
88
143
  }
144
+ /**
145
+ * Rewrite the ace file since the original one
146
+ * is importing ts-node which is not installed
147
+ * in a production environment.
148
+ */
89
149
  async #createAceFile(outDir) {
90
150
  const aceFileLocation = join(outDir, "ace.js");
91
151
  const aceFileContent = dedent(`
@@ -100,8 +160,21 @@ var Bundler = class {
100
160
  await fs.writeFile(aceFileLocation, aceFileContent);
101
161
  this.ui.logger.info("created ace file", { suffix: this.#getRelativeName(aceFileLocation) });
102
162
  }
163
+ /**
164
+ * Bundles the application to be run in production
165
+ *
166
+ * @param stopOnError - Whether to stop the build process on TypeScript errors
167
+ * @param client - Override the detected package manager
168
+ * @returns Promise that resolves to true if build succeeded, false otherwise
169
+ *
170
+ * @example
171
+ * const success = await bundler.bundle(true, 'npm')
172
+ */
103
173
  async bundle(stopOnError = true, client) {
104
174
  this.packageManager = client ?? await this.#detectPackageManager() ?? "npm";
175
+ /**
176
+ * Step 1: Parse config file to get the build output directory
177
+ */
105
178
  const config = parseConfig(this.cwd, this.#ts);
106
179
  if (!config) return false;
107
180
  this.ui.logger.info("loading hooks...");
@@ -111,17 +184,33 @@ var Bundler = class {
111
184
  "buildFinished"
112
185
  ]);
113
186
  this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
187
+ /**
188
+ * Step 2: Run init hook and the index generator
189
+ */
114
190
  await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
115
191
  this.#hooks.clear("init");
116
192
  this.ui.logger.info("generating indexes...");
117
193
  await this.#indexGenerator.generate();
194
+ /**
195
+ * Step 3: Cleanup existing build directory (if any)
196
+ */
118
197
  const outDir = config.options.outDir || fileURLToPath(new URL("build/", this.cwd));
119
198
  this.ui.logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
120
199
  await this.#cleanupBuildDirectory(outDir);
200
+ /**
201
+ * Step 4: Execute build starting hook
202
+ */
121
203
  await this.#hooks.runner("buildStarting").run(this);
204
+ /**
205
+ * Step 5: Build typescript source code
206
+ */
122
207
  this.ui.logger.info("compiling typescript source", { suffix: "tsc" });
123
208
  const buildCompleted = await this.#runTsc(outDir);
124
209
  await this.#createAceFile(outDir);
210
+ /**
211
+ * Remove incomplete build directory when tsc build
212
+ * failed and stopOnError is set to true.
213
+ */
125
214
  if (!buildCompleted && stopOnError) {
126
215
  await this.#cleanupBuildDirectory(outDir);
127
216
  const instructions = this.ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
@@ -130,48 +219,156 @@ var Bundler = class {
130
219
  this.ui.logger.logError(instructions.prepare());
131
220
  return false;
132
221
  }
222
+ /**
223
+ * Step 6: Copy meta files to the build directory
224
+ */
133
225
  const pkgFiles = ["package.json", ...SUPPORTED_PACKAGE_MANAGERS[this.packageManager].packageManagerFiles];
134
226
  this.ui.logger.info("copying meta files to the output directory");
135
227
  await this.#copyMetaFiles(outDir, pkgFiles);
136
228
  this.ui.logger.success("build completed");
137
229
  this.ui.logger.log("");
138
230
  const displayMessage = this.ui.instructions().heading("Run the following commands to start the server in production");
231
+ /**
232
+ * Step 7: Execute build completed hook
233
+ */
139
234
  await this.#hooks.runner("buildFinished").run(this, displayMessage);
235
+ /**
236
+ * Display next steps
237
+ */
140
238
  displayMessage.add(this.ui.colors.cyan(`cd ${this.#getRelativeName(outDir)}`)).add(this.ui.colors.cyan(SUPPORTED_PACKAGE_MANAGERS[this.packageManager].installCommand)).add(this.ui.colors.cyan("node bin/server.js")).render();
141
239
  return true;
142
240
  }
143
241
  };
242
+ //#endregion
243
+ //#region src/file_system.ts
144
244
  const DEFAULT_INCLUDES = ["**/*"];
145
245
  const ALWAYS_EXCLUDE = [
146
246
  ".git/**",
147
247
  "coverage/**",
148
248
  ".github/**",
149
- ".adonisjs/**"
249
+ ".adonisjs/**",
250
+ "tmp/**",
251
+ "storage/**",
252
+ "build/**"
150
253
  ];
151
254
  const DEFAULT_EXCLUDES = [
152
255
  "node_modules/**",
153
256
  "bower_components/**",
154
257
  "jspm_packages/**"
155
258
  ];
259
+ /**
260
+ * Exposes an intutive API to run actions when different kind of files
261
+ * are changed. The FileSystem is built around the vocabulary used by
262
+ * AdonisJS. Which includes:
263
+ *
264
+ * - Source files: TypeScript, JavaScript, JSON, JSX, TSX files that are included
265
+ * inside the "tsconfig.json" file are considered as source files.
266
+ * - Meta files: Files registered under the "metaFiles" array of "adonisrc.ts" file
267
+ * are called meta files.
268
+ * - Meta restart files: Meta files with "restart: true" enabled are called meta restart
269
+ * files.
270
+ *
271
+ * Using FileSystem you can register actions to be executed when a file changes in
272
+ * one of the above categories.
273
+ *
274
+ * @example
275
+ * const fs = new FileSystem(cwd, tsConfig, rcFile)
276
+ * const file = fs.inspect('./app/controllers/users_controller.ts')
277
+ */
156
278
  var FileSystem = class {
279
+ /**
280
+ * The current working project directory
281
+ */
157
282
  #cwd;
283
+ /**
284
+ * Referenced to the parsed ts config file. We use it to read the includes,
285
+ * excludes and pre-scanned files.
286
+ */
158
287
  #tsConfig;
288
+ /**
289
+ * Set of pre-scanned typeScript files provided by tsconfig
290
+ */
159
291
  #scannedTypeScriptFiles = /* @__PURE__ */ new Set();
292
+ /**
293
+ * Picomatch matcher function to know if a file path is
294
+ * part of includes
295
+ */
160
296
  #isIncluded;
297
+ /**
298
+ * Picomatch matcher function to know if a file path is
299
+ * part of excludes
300
+ */
161
301
  #isExcluded;
302
+ /**
303
+ * Picomatch matcher function to know if a file path is a
304
+ * meta file with reloadServer option enabled
305
+ */
162
306
  #isMetaFileWithReloadsEnabled;
307
+ /**
308
+ * Picomatch matcher function to know if a file path is a
309
+ * meta file with reloadServer option disabled
310
+ */
163
311
  #isMetaFileWithReloadsDisabled;
312
+ /**
313
+ * Picomatch matcher function to know if a file path is a
314
+ * test file or not
315
+ */
164
316
  #isTestFile;
317
+ /**
318
+ * References to includes and excludes glob patterns
319
+ */
165
320
  #includes;
166
321
  #excludes;
322
+ /**
323
+ * Includes glob patterns extracted from "tsconfig.json" file.
324
+ * Defaults to: ["**\/*"]
325
+ */
167
326
  get includes() {
168
327
  return this.#includes;
169
328
  }
329
+ /**
330
+ * Excludes glob patterns extracted from "tsconfig.json" file.
331
+ *
332
+ * Defaults to: [
333
+ * 'node_modules/**',
334
+ * 'bower_components/**',
335
+ * 'jspm_packages/**,
336
+ * ]
337
+ *
338
+ * Following patterns are always ignored
339
+ *
340
+ * '.git/**', 'coverage/**', '.github/**', '.adonisjs/**', 'tmp/**', 'storage/**', 'build/**'
341
+ */
170
342
  get excludes() {
171
343
  return this.#excludes;
172
344
  }
345
+ /**
346
+ * Inspect a file path to determine its type and properties within the project.
347
+ *
348
+ * This method analyzes a file to categorize it as a script file, test file, or meta file,
349
+ * and determines whether changes to the file should trigger server restarts. Results
350
+ * are memoized for performance optimization.
351
+ *
352
+ * @param absolutePath - The absolute Unix path to the file
353
+ * @param relativePath - The relative Unix path from the project root
354
+ * @returns File inspection result or null if the file should be ignored
355
+ *
356
+ * @example
357
+ * const file = fileSystem.inspect('/project/app/models/user.ts', 'app/models/user.ts')
358
+ * if (file) {
359
+ * console.log(file.fileType) // 'script'
360
+ * console.log(file.reloadServer) // true
361
+ * }
362
+ */
173
363
  inspect = memoize((absolutePath, relativePath) => {
174
364
  relativePath = relativePath ?? relative(this.#cwd, absolutePath);
365
+ /**
366
+ * If a file is a script file and part of the backend project, then we consider
367
+ * the file.
368
+ *
369
+ * Non script files are not checked inside the backend project, since there are
370
+ * anyways cannot be processed by TypeScript.
371
+ */
175
372
  if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
176
373
  debug_default("backend project file \"%s\"", relativePath);
177
374
  const isTestFile = this.#isTestFile(relativePath);
@@ -182,6 +379,9 @@ var FileSystem = class {
182
379
  unixAbsolutePath: absolutePath
183
380
  };
184
381
  }
382
+ /**
383
+ * Check for meta files with reload flag enabled
384
+ */
185
385
  if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
186
386
  debug_default("meta file \"%s\"", relativePath);
187
387
  return {
@@ -191,6 +391,9 @@ var FileSystem = class {
191
391
  unixAbsolutePath: absolutePath
192
392
  };
193
393
  }
394
+ /**
395
+ * Check for meta files with reload flag disabled
396
+ */
194
397
  if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
195
398
  debug_default("meta file \"%s\"", relativePath);
196
399
  return {
@@ -203,11 +406,38 @@ var FileSystem = class {
203
406
  debug_default("ignored file \"%s\"", relativePath);
204
407
  return null;
205
408
  });
409
+ /**
410
+ * Determines if a directory should be watched by the file watcher.
411
+ *
412
+ * This method checks if a directory should be monitored for file changes
413
+ * based on the TypeScript configuration includes/excludes patterns.
414
+ * Results are memoized for performance. Chokidar sends absolute Unix paths.
415
+ *
416
+ * Note: Use shouldWatchFile for files and this method for directories only.
417
+ *
418
+ * @param absolutePath - The absolute Unix path to the directory
419
+ * @returns True if the directory should be watched
420
+ *
421
+ * @example
422
+ * const shouldWatch = fileSystem.shouldWatchDirectory('/project/app/controllers')
423
+ * console.log(shouldWatch) // true
424
+ */
206
425
  shouldWatchDirectory = memoize((absolutePath) => {
426
+ /**
427
+ * Always watch the project root
428
+ */
207
429
  if (absolutePath === this.#cwd) {
208
430
  debug_default("watching project root");
209
431
  return true;
210
432
  }
433
+ /**
434
+ * Ignore directories excluded by tsconfig.json file. If tsconfig excludes
435
+ * directories include by metaFiles, then its a mis-configuration and we
436
+ * should update excludes to be more specific.
437
+ *
438
+ * Overriding excludes via metaFiles patterns is close to impossible because
439
+ * of how undeterministic glob patterns are.
440
+ */
211
441
  const relativePath = relative(this.#cwd, absolutePath);
212
442
  if (this.#isExcluded(relativePath)) {
213
443
  debug_default("watching \"%s\"", absolutePath);
@@ -215,6 +445,13 @@ var FileSystem = class {
215
445
  }
216
446
  return true;
217
447
  });
448
+ /**
449
+ * Create a new FileSystem instance
450
+ *
451
+ * @param cwd - The current working directory URL or string path
452
+ * @param tsConfig - Parsed TypeScript configuration
453
+ * @param rcFile - AdonisJS RC file configuration
454
+ */
218
455
  constructor(cwd, tsConfig, rcFile) {
219
456
  this.#cwd = cwd;
220
457
  this.#tsConfig = tsConfig;
@@ -222,15 +459,27 @@ var FileSystem = class {
222
459
  const metaFiles = rcFile.metaFiles ?? [];
223
460
  const testSuites = rcFile.suites ?? [];
224
461
  const outDir = tsConfig.config.compilerOptions?.outDir;
462
+ /**
463
+ * Register files we know ahead of time
464
+ */
225
465
  for (const file of files) this.#scannedTypeScriptFiles.add(string.toUnixSlash(file));
466
+ /**
467
+ * Compute includes and excludes
468
+ */
226
469
  this.#includes = tsConfig.config.include || DEFAULT_INCLUDES;
227
470
  this.#excludes = ALWAYS_EXCLUDE.concat(tsConfig.config.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES));
471
+ /**
472
+ * Pre-compute meta file patterns to avoid repeated array operations
473
+ */
228
474
  const metaFilesWithReloads = [];
229
475
  const metaFilesWithoutReloads = [];
230
476
  for (const file of metaFiles) if (file.reloadServer) metaFilesWithReloads.push(file.pattern);
231
477
  else metaFilesWithoutReloads.push(file.pattern);
232
478
  const testFilePatterns = testSuites.flatMap((suite) => suite.files);
233
479
  const picomatcchOptions = { cwd: this.#cwd };
480
+ /**
481
+ * Initiate picomatch matchers we will need to identify metafiles
482
+ */
234
483
  this.#isMetaFileWithReloadsEnabled = picomatch(metaFilesWithReloads, picomatcchOptions);
235
484
  this.#isMetaFileWithReloadsDisabled = picomatch(metaFilesWithoutReloads, picomatcchOptions);
236
485
  this.#isTestFile = picomatch(testFilePatterns, picomatcchOptions);
@@ -245,32 +494,104 @@ var FileSystem = class {
245
494
  testSuites
246
495
  });
247
496
  }
497
+ /**
498
+ * Determines if a file path represents a script file based on TypeScript configuration.
499
+ *
500
+ * Script files are those that can be processed by the TypeScript compiler:
501
+ * - Files ending with ".ts" or ".tsx" (excluding ".d.ts" declaration files)
502
+ * - Files ending with ".js" when "allowJs" option is enabled in tsconfig
503
+ * - Files ending with ".json" when "resolveJsonModule" option is enabled in tsconfig
504
+ *
505
+ * @param relativePath - The relative file path to check
506
+ * @returns True if the file is a script file
507
+ */
248
508
  #isScriptFile(relativePath) {
249
509
  if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) return true;
250
510
  if (this.#tsConfig.config.compilerOptions?.allowJs && relativePath.endsWith(".js")) return true;
251
511
  if (this.#tsConfig.config.compilerOptions?.resolveJsonModule && relativePath.endsWith(".json")) return true;
252
512
  return false;
253
513
  }
514
+ /**
515
+ * Checks if a file path is part of the backend TypeScript project.
516
+ *
517
+ * Uses TypeScript configuration "includes", "excludes", and "files" paths
518
+ * to determine if a file should be considered part of the project compilation.
519
+ *
520
+ * @param relativePath - The relative file path to check
521
+ * @returns True if the file is part of the backend project
522
+ */
254
523
  #isPartOfBackendProject(relativePath) {
524
+ /**
525
+ * Script and non-script files can be excluded using tsconfig
526
+ */
255
527
  if (this.#isExcluded(relativePath)) {
256
528
  debug_default("excluded by tsconfig \"%s\"", relativePath);
257
529
  return false;
258
530
  }
531
+ /**
532
+ * Return true when included
533
+ */
259
534
  if (this.#isIncluded(relativePath)) {
260
535
  debug_default("included by tsconfig \"%s\"", relativePath);
261
536
  return true;
262
537
  }
263
538
  return false;
264
539
  }
540
+ /**
541
+ * Returns true if the file should be watched. Chokidar sends
542
+ * absolute unix paths to the ignored callback.
543
+ *
544
+ * You must use "shouldWatchDirectory" method for directories and call
545
+ * this method for files only.
546
+ *
547
+ * @param absolutePath - The absolute path to the file
548
+ * @returns True if the file should be watched
549
+ */
265
550
  shouldWatchFile(absolutePath) {
266
551
  return this.inspect(absolutePath) !== null;
267
552
  }
268
553
  };
554
+ //#endregion
555
+ //#region src/shortcuts_manager.ts
556
+ /**
557
+ * Manages keyboard shortcuts for development server interaction.
558
+ *
559
+ * The ShortcutsManager provides a convenient way to handle keyboard inputs
560
+ * during development, allowing users to restart servers, clear console,
561
+ * open browsers, and perform other common development tasks through simple
562
+ * key presses.
563
+ *
564
+ * @example
565
+ * const shortcuts = new ShortcutsManager({
566
+ * logger: ui.logger,
567
+ * callbacks: {
568
+ * onRestart: () => server.restart(),
569
+ * onClear: () => console.clear(),
570
+ * onQuit: () => process.exit()
571
+ * }
572
+ * })
573
+ * shortcuts.setup()
574
+ */
269
575
  var ShortcutsManager = class {
576
+ /**
577
+ * Logger instance for displaying messages
578
+ */
270
579
  #logger;
580
+ /**
581
+ * Callback functions for different keyboard shortcuts
582
+ */
271
583
  #callbacks;
584
+ /**
585
+ * The server URL used for opening browser
586
+ */
272
587
  #serverUrl;
588
+ /**
589
+ * Key press event handler function
590
+ */
273
591
  #keyPressHandler;
592
+ /**
593
+ * Available keyboard shortcuts with their handlers
594
+ */
274
595
  #shortcuts = [
275
596
  {
276
597
  key: "r",
@@ -300,35 +621,83 @@ var ShortcutsManager = class {
300
621
  handler: () => this.showHelp()
301
622
  }
302
623
  ];
624
+ /**
625
+ * Create a new ShortcutsManager instance
626
+ *
627
+ * @param options - Configuration options for the shortcuts manager
628
+ */
303
629
  constructor(options) {
304
630
  this.#logger = options.logger;
305
631
  this.#callbacks = options.callbacks;
306
632
  }
633
+ /**
634
+ * Set server url for opening in browser
635
+ *
636
+ * This URL will be used when the user presses 'o' to open the
637
+ * development server in their default browser.
638
+ *
639
+ * @param url - The server URL to open when 'o' key is pressed
640
+ */
307
641
  setServerUrl(url) {
308
642
  this.#serverUrl = url;
309
643
  }
644
+ /**
645
+ * Initialize keyboard shortcuts by setting up raw mode on stdin
646
+ *
647
+ * This method enables raw mode on stdin to capture individual keypresses
648
+ * and sets up the event listener for handling keyboard input. Only works
649
+ * in TTY environments.
650
+ */
310
651
  setup() {
311
652
  if (!process.stdin.isTTY) return;
312
653
  process.stdin.setRawMode(true);
313
654
  this.#keyPressHandler = (data) => this.#handleKeyPress(data.toString());
314
655
  process.stdin.on("data", this.#keyPressHandler);
315
656
  }
657
+ /**
658
+ * Handle key press events and execute corresponding shortcuts
659
+ *
660
+ * Processes individual key presses and matches them against registered
661
+ * shortcuts. Also handles special key combinations like Ctrl+C and Ctrl+D.
662
+ *
663
+ * @param key - The pressed key as a string
664
+ */
316
665
  #handleKeyPress(key) {
317
666
  if (key === "" || key === "") return this.#callbacks.onQuit();
318
667
  const shortcut = this.#shortcuts.find((s) => s.key === key);
319
668
  if (shortcut) shortcut.handler();
320
669
  }
670
+ /**
671
+ * Handle opening browser with the configured server URL
672
+ *
673
+ * Uses the 'open' package to launch the default browser and navigate
674
+ * to the development server URL.
675
+ */
321
676
  async #handleOpenBrowser() {
322
677
  this.#logger.log("");
323
678
  this.#logger.info(`Opening ${this.#serverUrl}...`);
324
679
  const { default: open } = await import("open");
325
680
  open(this.#serverUrl);
326
681
  }
682
+ /**
683
+ * Show available keyboard shortcuts in the console
684
+ *
685
+ * Displays a formatted list of all available keyboard shortcuts
686
+ * and their descriptions to help users understand what actions
687
+ * are available.
688
+ */
327
689
  showHelp() {
328
690
  this.#logger.log("");
329
691
  this.#logger.log("Available shortcuts:");
330
692
  this.#shortcuts.forEach(({ key, description }) => this.#logger.log(`· ${key}: ${description}`));
331
693
  }
694
+ /**
695
+ * Cleanup keyboard shortcuts and restore terminal state
696
+ *
697
+ * Disables raw mode on stdin, removes event listeners, and restores
698
+ * the terminal to its normal state. Should be called when shutting down
699
+ * the development server.
700
+ */
332
701
  cleanup() {
333
702
  if (!process.stdin.isTTY) return;
334
703
  process.stdin.setRawMode(false);
@@ -338,36 +707,111 @@ var ShortcutsManager = class {
338
707
  this.#keyPressHandler = void 0;
339
708
  }
340
709
  };
710
+ //#endregion
711
+ //#region src/dev_server.ts
712
+ /**
713
+ * Exposes the API to start the development server in HMR, watch or static mode
714
+ *
715
+ * In HMR mode, the DevServer will exec the "bin/server.ts" file and let hot-hook
716
+ * manage the changes using hot module reloading.
717
+ *
718
+ * In watch mode, the DevServer will start an internal watcher and restarts the server
719
+ * after every file change. The files must be part of the TypeScript project (via tsconfig.json),
720
+ * or registered as metaFiles.
721
+ *
722
+ * In static mode, the server runs without file watching or hot reloading.
723
+ *
724
+ * @example
725
+ * const devServer = new DevServer(cwd, { hmr: true, hooks: [] })
726
+ * await devServer.start(ts)
727
+ */
341
728
  var DevServer = class DevServer {
729
+ /**
730
+ * Pre-allocated info object for hot-hook full reload events
731
+ */
342
732
  static #HOT_HOOK_FULL_RELOAD_INFO = {
343
733
  source: "hot-hook",
344
734
  fullReload: true,
345
735
  hotReloaded: false
346
736
  };
737
+ /**
738
+ * Pre-allocated info object for hot-hook invalidation events
739
+ */
347
740
  static #HOT_HOOK_INVALIDATED_INFO = {
348
741
  source: "hot-hook",
349
742
  fullReload: false,
350
743
  hotReloaded: true
351
744
  };
745
+ /**
746
+ * Pre-allocated info object for file watcher events
747
+ */
352
748
  static #WATCHER_INFO = {
353
749
  source: "watcher",
354
750
  fullReload: true,
355
751
  hotReloaded: false
356
752
  };
753
+ /**
754
+ * External listeners that are invoked when child process
755
+ * gets an error or closes
756
+ */
357
757
  #onError;
358
758
  #onClose;
759
+ /**
760
+ * The stickyPort is set by the start and the startAndWatch methods
761
+ * and we will continue to use that port during restart
762
+ */
359
763
  #stickyPort;
764
+ /**
765
+ * The stickyHmrPort is set by the start and the startAndWatch methods
766
+ * and we will continue to use that port during restart
767
+ */
360
768
  #stickyHmrPort;
769
+ /**
770
+ * The mode is set by the start and the startAndWatch methods
771
+ */
361
772
  #mode = "static";
773
+ /**
774
+ * Reference to chokidar watcher
775
+ */
362
776
  #watcher;
777
+ /**
778
+ * Reference to the child process
779
+ */
363
780
  #httpServer;
781
+ /**
782
+ * Flag to track if the HTTP server child process is alive
783
+ */
364
784
  #isHttpServerAlive = false;
785
+ /**
786
+ * Keyboard shortcuts manager instance
787
+ */
365
788
  #shortcutsManager;
789
+ /**
790
+ * Filesystem is used to decide which files to watch or entertain when
791
+ * using hot-hook
792
+ */
366
793
  #fileSystem;
794
+ /**
795
+ * Index generator for managing auto-generated index files
796
+ */
367
797
  #indexGenerator;
798
+ /**
799
+ * Routes scanner to scan routes and infer route request and
800
+ * response data
801
+ */
368
802
  #routesScanner;
803
+ /**
804
+ * Hooks to execute custom actions during the dev server lifecycle
805
+ */
369
806
  #hooks;
807
+ /**
808
+ * CLI UI instance for displaying colorful messages and progress information
809
+ */
370
810
  ui = cliui();
811
+ /**
812
+ * Restarts the HTTP server and throttle concurrent calls to
813
+ * ensure we do not end up with a long loop of restarts
814
+ */
371
815
  #restartHTTPServer = throttle(async () => {
372
816
  if (this.#httpServer) {
373
817
  this.#httpServer.removeAllListeners();
@@ -375,6 +819,12 @@ var DevServer = class DevServer {
375
819
  }
376
820
  await this.#startHTTPServer(this.#stickyPort, this.#stickyHmrPort);
377
821
  }, "restartHTTPServer");
822
+ /**
823
+ * Sets up keyboard shortcuts for development server interactions
824
+ *
825
+ * Initializes the shortcuts manager with callbacks for restarting the server,
826
+ * clearing the screen, and quitting the application.
827
+ */
378
828
  #setupKeyboardShortcuts() {
379
829
  this.#shortcutsManager = new ShortcutsManager({
380
830
  logger: this.ui.logger,
@@ -386,27 +836,85 @@ var DevServer = class DevServer {
386
836
  });
387
837
  this.#shortcutsManager.setup();
388
838
  }
839
+ /**
840
+ * Cleanup keyboard shortcuts and restore terminal state
841
+ *
842
+ * Removes keyboard shortcuts event listeners and restores the terminal
843
+ * to its normal state when shutting down the development server.
844
+ */
389
845
  #cleanupKeyboardShortcuts() {
390
846
  this.#shortcutsManager?.cleanup();
391
847
  }
848
+ /**
849
+ * The mode in which the DevServer is running
850
+ *
851
+ * Returns the current operating mode of the development server:
852
+ * - 'hmr': Hot Module Reloading enabled
853
+ * - 'watch': File system watching with full restarts
854
+ * - 'static': No file watching or hot reloading
855
+ */
392
856
  get mode() {
393
857
  return this.#mode;
394
858
  }
859
+ /**
860
+ * Script file to start the development server
861
+ */
395
862
  scriptFile = "bin/server.ts";
863
+ /**
864
+ * The current working directory URL
865
+ */
396
866
  cwd;
867
+ /**
868
+ * File path computed from the cwd
869
+ */
397
870
  cwdPath;
871
+ /**
872
+ * Development server configuration options including hooks and environment variables
873
+ */
398
874
  options;
875
+ /**
876
+ * Create a new DevServer instance
877
+ *
878
+ * @param cwd - The current working directory URL
879
+ * @param options - Development server configuration options
880
+ */
399
881
  constructor(cwd, options) {
400
882
  this.cwd = cwd;
401
883
  this.options = options;
402
884
  this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
403
885
  }
886
+ /**
887
+ * Type guard to check if child process message is from AdonisJS HTTP server
888
+ *
889
+ * Validates that a message from the child process contains the expected
890
+ * structure indicating the AdonisJS server is ready and listening.
891
+ *
892
+ * @param message - Unknown message from child process
893
+ * @returns True if message is an AdonisJS ready message
894
+ */
404
895
  #isAdonisJSReadyMessage(message) {
405
896
  return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
406
897
  }
898
+ /**
899
+ * Type guard to check if child process message contains routes information
900
+ *
901
+ * Validates that a message from the child process contains the expected
902
+ * structure with routes file location from the AdonisJS server.
903
+ *
904
+ * @param message - Unknown message from child process
905
+ * @returns True if message contains routes file location
906
+ */
407
907
  #isAdonisJSRoutesMessage(message) {
408
908
  return message !== null && typeof message === "object" && "routesFileLocation" in message;
409
909
  }
910
+ /**
911
+ * Displays server information and executes hooks after server startup
912
+ *
913
+ * Shows server URL, mode, startup duration, and help instructions.
914
+ * Also executes the devServerStarted hooks to allow custom post-startup logic.
915
+ *
916
+ * @param message - Server ready message containing port, host, and optional duration
917
+ */
410
918
  async #postServerReady(message) {
411
919
  const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
412
920
  const info = {
@@ -418,6 +926,10 @@ var DevServer = class DevServer {
418
926
  const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(serverUrl)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
419
927
  if (message.duration) displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
420
928
  displayMessage.add(`Press ${this.ui.colors.dim("h")} to show help`);
929
+ /**
930
+ * Run hooks before displaying the "displayMessage". It will allow hooks to add
931
+ * custom lines to the display message.
932
+ */
421
933
  try {
422
934
  await this.#hooks.runner("devServerStarted").run(this, info, displayMessage);
423
935
  } catch (error) {
@@ -426,12 +938,30 @@ var DevServer = class DevServer {
426
938
  }
427
939
  displayMessage.render();
428
940
  }
941
+ /**
942
+ * Type guard to check if child process message is from hot-hook
943
+ *
944
+ * Validates that a message from the child process is a hot-hook notification
945
+ * about file changes, invalidations, or full reloads.
946
+ *
947
+ * @param message - Unknown message from child process
948
+ * @returns True if message is a hot-hook message
949
+ */
429
950
  #isHotHookMessage(message) {
430
951
  return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
431
952
  }
953
+ /**
954
+ * Conditionally clears the terminal screen based on configuration
955
+ *
956
+ * Clears the terminal screen if the clearScreen option is enabled,
957
+ * providing a clean view for development output.
958
+ */
432
959
  #clearScreen() {
433
960
  if (this.options.clearScreen) process.stdout.write("\x1Bc");
434
961
  }
962
+ /**
963
+ * Creates our file system watcher
964
+ */
435
965
  #createWatcher(options) {
436
966
  const watcher = watch({
437
967
  usePolling: options?.poll ?? false,
@@ -455,6 +985,10 @@ var DevServer = class DevServer {
455
985
  });
456
986
  return watcher;
457
987
  }
988
+ /**
989
+ * Handles file change events in HMR mode by forwarding to hot-hook
990
+ * or restarting the server if dead.
991
+ */
458
992
  #handleHmrWatcherEvent(options) {
459
993
  const relativePath = string.toUnixSlash(options.filePath);
460
994
  const absolutePath = join(this.cwdPath, relativePath);
@@ -464,24 +998,59 @@ var DevServer = class DevServer {
464
998
  this.#restartHTTPServer();
465
999
  return;
466
1000
  }
1001
+ /**
1002
+ * For add/unlink, we call the hooks directly since hot-hook ignores files
1003
+ * not in its dependency tree. This ensures index files are regenerated
1004
+ * for new/removed files.
1005
+ */
467
1006
  if (options.action === "add") this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
468
1007
  else if (options.action === "unlink") this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1008
+ /**
1009
+ * Forward all events to hot-hook so it can:
1010
+ * - Update its dependency tree (for unlink)
1011
+ * - Handle HMR for change events on imported files
1012
+ * - Then we wait for hot-hook to notify us back via IPC message
1013
+ */
469
1014
  this.#httpServer?.send({
470
1015
  type: "hot-hook:file-changed",
471
1016
  path: absolutePath,
472
1017
  action: options.action
473
1018
  });
474
1019
  }
1020
+ /**
1021
+ * Handles file change events and triggers appropriate server actions
1022
+ *
1023
+ * Processes file change notifications and determines whether to restart
1024
+ * the server, hot reload, or ignore the change based on file type and mode.
1025
+ *
1026
+ * @param relativePath - Relative path to the changed file
1027
+ * @param absolutePath - Absolute path to the changed file
1028
+ * @param action - Type of file change (add, update, delete)
1029
+ * @param info - Optional information about the change source and reload behavior
1030
+ */
475
1031
  #handleFileChange(relativePath, absolutePath, action, info) {
1032
+ /**
1033
+ * Ignore add and delete events in HMR mode and let hot-hook find the
1034
+ * file via import first.
1035
+ *
1036
+ * Remember, hot-hook does not send the action as "add" or "delete" if this
1037
+ * file is being imported.
1038
+ */
476
1039
  if ((action === "add" || action === "delete") && this.mode === "hmr") {
477
1040
  debug_default("ignoring add and delete actions in HMR mode %s", relativePath);
478
1041
  return;
479
1042
  }
1043
+ /**
1044
+ * Notify about the invalidated file
1045
+ */
480
1046
  if (info && info.source === "hot-hook" && info.hotReloaded) {
481
1047
  debug_default("hot reloading %s, info %O", relativePath, info);
482
1048
  this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${relativePath}`);
483
1049
  return;
484
1050
  }
1051
+ /**
1052
+ * Do not do anything when fullReload is not enabled.
1053
+ */
485
1054
  if (info && !info.fullReload) {
486
1055
  debug_default("ignoring full reload", relativePath, info);
487
1056
  return;
@@ -496,10 +1065,30 @@ var DevServer = class DevServer {
496
1065
  }
497
1066
  this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
498
1067
  }
1068
+ /**
1069
+ * Regenerates index files when a file is added or removed
1070
+ *
1071
+ * Updates the index generator to reflect file system changes by adding
1072
+ * or removing files from the generated index files.
1073
+ *
1074
+ * @param filePath - Absolute path to the file that changed
1075
+ * @param action - Whether the file was added or deleted
1076
+ */
499
1077
  #regenerateIndex(filePath, action) {
500
1078
  if (action === "add") return this.#indexGenerator.addFile(filePath);
501
1079
  return this.#indexGenerator.removeFile(filePath);
502
1080
  }
1081
+ /**
1082
+ * Re-scans routes when a file is modified during hot reloading
1083
+ *
1084
+ * Invalidates the routes cache for the given file and triggers route
1085
+ * scanning hooks if the invalidation was successful.
1086
+ *
1087
+ * @param filePath - Absolute path to the file that was modified
1088
+ *
1089
+ * @example
1090
+ * await devServer.#reScanRoutes('/path/to/routes.ts')
1091
+ */
503
1092
  async #reScanRoutes(filePath) {
504
1093
  if (!this.#routesScanner) return;
505
1094
  try {
@@ -509,18 +1098,47 @@ var DevServer = class DevServer {
509
1098
  this.ui.logger.fatal(error);
510
1099
  }
511
1100
  }
1101
+ /**
1102
+ * Processes routes received from the AdonisJS server
1103
+ *
1104
+ * Executes routesCommitted hooks and optionally scans routes if scanning
1105
+ * hooks are registered. Creates a routes scanner instance if needed and
1106
+ * processes routes for each domain.
1107
+ *
1108
+ * @param routesList - Routes organized by domain
1109
+ *
1110
+ * @example
1111
+ * await devServer.#processRoutes({
1112
+ * 'example.com': [
1113
+ * { pattern: '/', handler: 'HomeController.index' }
1114
+ * ]
1115
+ * })
1116
+ */
512
1117
  #processRoutes = throttle(async (routesFileLocation) => {
513
1118
  try {
514
1119
  const scanRoutes = this.#hooks.has("routesScanning") || this.#hooks.has("routesScanned");
515
1120
  const shareRoutes = this.#hooks.has("routesCommitted");
1121
+ /**
1122
+ * Remove the routes file and return early when there are no
1123
+ * hooks listening for routes related events
1124
+ */
516
1125
  if (!scanRoutes && !shareRoutes) {
517
1126
  unlink(routesFileLocation).catch(() => {});
518
1127
  return;
519
1128
  }
1129
+ /**
1130
+ * Read routes JSON, parse it and remove the file
1131
+ */
520
1132
  const routesJSON = await readFile(routesFileLocation, "utf-8");
521
1133
  const routesList = JSON.parse(routesJSON);
522
1134
  unlink(routesFileLocation).catch(() => {});
1135
+ /**
1136
+ * Notify about the existence of routes
1137
+ */
523
1138
  if (shareRoutes) await this.#hooks.runner("routesCommitted").run(this, routesList);
1139
+ /**
1140
+ * Scan routes and notify scanning and scanned hooks
1141
+ */
524
1142
  if (scanRoutes) {
525
1143
  this.#routesScanner = new RoutesScanner(this.cwdPath, []);
526
1144
  await this.#hooks.runner("routesScanning").run(this, this.#routesScanner);
@@ -532,12 +1150,22 @@ var DevServer = class DevServer {
532
1150
  this.ui.logger.fatal(error);
533
1151
  }
534
1152
  }, "processRoutes");
1153
+ /**
1154
+ * Registers hooks for file system events and server restart triggers
1155
+ *
1156
+ * Sets up event handlers that respond to file additions, changes, and removals
1157
+ * by regenerating indexes and handling server restarts as needed.
1158
+ */
535
1159
  #registerServerRestartHooks() {
536
1160
  this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
537
1161
  this.#regenerateIndex(absolutePath, "add");
538
1162
  this.#handleFileChange(relativePath, absolutePath, "add");
539
1163
  });
540
1164
  this.#hooks.add("fileChanged", (relativePath, absolutePath, info) => {
1165
+ /**
1166
+ * Rescan routes when the file is hot-reloaded or when the file is
1167
+ * not part of the imports tree, but meant to be hot-reloaded
1168
+ */
541
1169
  if (info.hotReloaded || !info.hotReloaded && !info.fullReload) this.#reScanRoutes(absolutePath);
542
1170
  this.#handleFileChange(relativePath, absolutePath, "update", info);
543
1171
  });
@@ -546,6 +1174,23 @@ var DevServer = class DevServer {
546
1174
  this.#handleFileChange(relativePath, absolutePath, "delete");
547
1175
  });
548
1176
  }
1177
+ /**
1178
+ * Initializes the development server state and executes init hooks
1179
+ *
1180
+ * Parses TypeScript configuration, sets up file system, loads hooks,
1181
+ * initializes the index generator, and prepares the server for the
1182
+ * specified mode (HMR, watch, or static).
1183
+ *
1184
+ * @param ts - TypeScript module reference
1185
+ * @param mode - Server mode (hmr, watch, or static)
1186
+ * @returns True if initialization succeeds, false if tsconfig parsing fails
1187
+ *
1188
+ * @example
1189
+ * const success = await devServer.#init(ts, 'hmr')
1190
+ * if (!success) {
1191
+ * console.error('Failed to initialize dev server')
1192
+ * }
1193
+ */
549
1194
  async #init(mode) {
550
1195
  const tsConfig = readTsConfig(this.cwdPath);
551
1196
  if (!tsConfig) {
@@ -573,16 +1218,40 @@ var DevServer = class DevServer {
573
1218
  ]);
574
1219
  this.#registerServerRestartHooks();
575
1220
  this.#setupKeyboardShortcuts();
1221
+ /**
1222
+ * Run init hooks and clear them as they won't be executed
1223
+ * ever again
1224
+ */
576
1225
  await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
577
1226
  this.#hooks.clear("init");
578
1227
  this.ui.logger.info("generating indexes...");
579
1228
  await this.#indexGenerator.generate();
580
1229
  return true;
581
1230
  }
1231
+ /**
1232
+ * Starts the HTTP server as a child process
1233
+ *
1234
+ * Creates a new Node.js child process to run the server script with the
1235
+ * specified port and configuration. Sets up message handlers for server
1236
+ * ready notifications, routes sharing, and hot-hook events. Executes
1237
+ * devServerStarting hooks before spawning the process.
1238
+ *
1239
+ * @param port - Port number for the server to listen on
1240
+ *
1241
+ * @example
1242
+ * await devServer.#startHTTPServer('3333')
1243
+ */
582
1244
  async #startHTTPServer(port, hmrPort) {
1245
+ /**
1246
+ * Execute the registered before creating the child process. This will allow
1247
+ * hooks to modify the options before they are used.
1248
+ */
583
1249
  await this.#hooks.runner("devServerStarting").run(this);
584
1250
  debug_default("starting http server using \"%s\" file, options %O", this.scriptFile, this.options);
585
1251
  return new Promise((resolve) => {
1252
+ /**
1253
+ * Creating child process
1254
+ */
586
1255
  this.#httpServer = runNode(this.cwd, {
587
1256
  script: this.scriptFile,
588
1257
  env: {
@@ -630,14 +1299,52 @@ var DevServer = class DevServer {
630
1299
  });
631
1300
  });
632
1301
  }
1302
+ /**
1303
+ * Adds listener to get notified when dev server is closed
1304
+ *
1305
+ * Registers a callback function that will be invoked when the development
1306
+ * server's child process exits. The callback receives the exit code.
1307
+ *
1308
+ * @param callback - Function to call when dev server closes
1309
+ * @returns This DevServer instance for method chaining
1310
+ *
1311
+ * @example
1312
+ * devServer.onClose((exitCode) => {
1313
+ * console.log(`Server closed with exit code: ${exitCode}`)
1314
+ * })
1315
+ */
633
1316
  onClose(callback) {
634
1317
  this.#onClose = callback;
635
1318
  return this;
636
1319
  }
1320
+ /**
1321
+ * Adds listener to get notified when dev server encounters an error
1322
+ *
1323
+ * Registers a callback function that will be invoked when the development
1324
+ * server's child process encounters an error or fails to start.
1325
+ *
1326
+ * @param callback - Function to call when dev server encounters an error
1327
+ * @returns This DevServer instance for method chaining
1328
+ *
1329
+ * @example
1330
+ * devServer.onError((error) => {
1331
+ * console.error('Dev server error:', error.message)
1332
+ * })
1333
+ */
637
1334
  onError(callback) {
638
1335
  this.#onError = callback;
639
1336
  return this;
640
1337
  }
1338
+ /**
1339
+ * Closes watchers and terminates the running child process
1340
+ *
1341
+ * Cleans up keyboard shortcuts, stops file system watchers, and kills
1342
+ * the HTTP server child process. This should be called when shutting down
1343
+ * the development server.
1344
+ *
1345
+ * @example
1346
+ * await devServer.close()
1347
+ */
641
1348
  async close() {
642
1349
  this.#cleanupKeyboardShortcuts();
643
1350
  await this.#watcher?.close();
@@ -646,6 +1353,19 @@ var DevServer = class DevServer {
646
1353
  this.#httpServer.kill("SIGKILL");
647
1354
  }
648
1355
  }
1356
+ /**
1357
+ * Starts the development server in static or HMR mode
1358
+ *
1359
+ * Initializes the server and starts the HTTP server. The mode is determined
1360
+ * by the `hmr` option in DevServerOptions. In HMR mode, hot-hook is configured
1361
+ * to enable hot module reloading.
1362
+ *
1363
+ * @param ts - TypeScript module reference
1364
+ *
1365
+ * @example
1366
+ * const devServer = new DevServer(cwd, { hmr: true, hooks: [] })
1367
+ * await devServer.start(ts)
1368
+ */
649
1369
  async start() {
650
1370
  if (!await this.#init(this.options.hmr ? "hmr" : "static")) return;
651
1371
  if (this.#mode === "hmr") {
@@ -681,6 +1401,21 @@ var DevServer = class DevServer {
681
1401
  });
682
1402
  });
683
1403
  }
1404
+ /**
1405
+ * Starts the development server in watch mode and restarts on file changes
1406
+ *
1407
+ * Initializes the server, starts the HTTP server, and sets up a file system
1408
+ * watcher that monitors for changes. When files are added, modified, or deleted,
1409
+ * the server automatically restarts. The watcher respects TypeScript project
1410
+ * configuration and metaFiles settings.
1411
+ *
1412
+ * @param ts - TypeScript module reference
1413
+ * @param options - Watch options including polling mode
1414
+ *
1415
+ * @example
1416
+ * const devServer = new DevServer(cwd, { hooks: [] })
1417
+ * await devServer.startAndWatch(ts, { poll: false })
1418
+ */
684
1419
  async startAndWatch(options) {
685
1420
  if (!await this.#init("watch")) return;
686
1421
  this.ui.logger.info("starting HTTP server...");
@@ -703,17 +1438,77 @@ var DevServer = class DevServer {
703
1438
  });
704
1439
  }
705
1440
  };
1441
+ //#endregion
1442
+ //#region src/test_runner.ts
1443
+ /**
1444
+ * Exposes the API to run Japa tests and optionally watch for file
1445
+ * changes to re-run the tests.
1446
+ *
1447
+ * The TestRunner provides intelligent test execution with watch mode capabilities.
1448
+ * When files change, it can selectively run specific tests or the entire suite
1449
+ * based on what changed.
1450
+ *
1451
+ * The watch mode functions as follows:
1452
+ * - If the changed file is a test file, then only tests for that file
1453
+ * will be re-run.
1454
+ * - Otherwise, all tests will re-run with respect to the initial
1455
+ * filters applied when running the `node ace test` command.
1456
+ *
1457
+ * @example
1458
+ * const testRunner = new TestRunner(cwd, { suites: [], hooks: [] })
1459
+ * await testRunner.run()
1460
+ *
1461
+ * @example
1462
+ * // Run tests in watch mode
1463
+ * const testRunner = new TestRunner(cwd, { suites: [], hooks: [] })
1464
+ * await testRunner.runAndWatch(ts, { poll: false })
1465
+ */
706
1466
  var TestRunner = class {
1467
+ /**
1468
+ * External listeners that are invoked when child process
1469
+ * gets an error or closes
1470
+ */
707
1471
  #onError;
708
1472
  #onClose;
1473
+ /**
1474
+ * The stickyPort is set by the startAndWatch method and we will
1475
+ * continue to use this port during re-runs
1476
+ */
709
1477
  #stickyPort;
1478
+ /**
1479
+ * The stickyHmrPort is set by the start and the startAndWatch methods
1480
+ * and we will continue to use that port during restart
1481
+ */
710
1482
  #stickyHmrPort;
1483
+ /**
1484
+ * Reference to chokidar watcher
1485
+ */
711
1486
  #watcher;
1487
+ /**
1488
+ * Reference to the test script child process
1489
+ */
712
1490
  #testsProcess;
1491
+ /**
1492
+ * Filesystem is used to decide which files to watch or entertain in watch
1493
+ * mode
1494
+ */
713
1495
  #fileSystem;
1496
+ /**
1497
+ * Hooks to execute custom actions during the tests runner lifecycle
1498
+ */
714
1499
  #hooks;
1500
+ /**
1501
+ * Index generator for managing auto-generated index files
1502
+ */
715
1503
  #indexGenerator;
1504
+ /**
1505
+ * CLI UI instance for displaying colorful messages and progress information
1506
+ */
716
1507
  ui = cliui();
1508
+ /**
1509
+ * Re-runs the test child process and throttle concurrent calls to
1510
+ * ensure we do not end up with a long loop of restarts
1511
+ */
717
1512
  #reRunTests = throttle(async (filters) => {
718
1513
  if (this.#testsProcess) {
719
1514
  this.#testsProcess.removeAllListeners();
@@ -721,15 +1516,41 @@ var TestRunner = class {
721
1516
  }
722
1517
  await this.#runTests(this.#stickyPort, this.#stickyHmrPort, filters);
723
1518
  }, "reRunTests");
1519
+ /**
1520
+ * The script file to run as a child process
1521
+ */
724
1522
  scriptFile = "bin/test.ts";
1523
+ /**
1524
+ * The current working directory URL
1525
+ */
725
1526
  cwd;
1527
+ /**
1528
+ * The current working directory path as a string
1529
+ */
726
1530
  cwdPath;
1531
+ /**
1532
+ * Test runner configuration options including filters, reporters, and hooks
1533
+ */
727
1534
  options;
1535
+ /**
1536
+ * Create a new TestRunner instance
1537
+ *
1538
+ * @param cwd - The current working directory URL
1539
+ * @param options - Test runner configuration options
1540
+ */
728
1541
  constructor(cwd, options) {
729
1542
  this.cwd = cwd;
730
1543
  this.options = options;
731
1544
  this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
732
1545
  }
1546
+ /**
1547
+ * Convert test runner options to the CLI args
1548
+ *
1549
+ * Transforms the test runner configuration options into command-line
1550
+ * arguments that can be passed to the test script.
1551
+ *
1552
+ * @returns Array of command-line arguments
1553
+ */
733
1554
  #convertOptionsToArgs() {
734
1555
  const args = [];
735
1556
  if (this.options.reporters) {
@@ -747,6 +1568,15 @@ var TestRunner = class {
747
1568
  }
748
1569
  return args;
749
1570
  }
1571
+ /**
1572
+ * Converts all known filters to CLI args
1573
+ *
1574
+ * Transforms test filters (suites, files, groups, tags, tests) into
1575
+ * command-line arguments for the test script.
1576
+ *
1577
+ * @param filters - The test filters to convert
1578
+ * @returns Array of command-line arguments representing the filters
1579
+ */
750
1580
  #convertFiltersToArgs(filters) {
751
1581
  const args = [];
752
1582
  if (filters.suites) args.push(...filters.suites);
@@ -768,13 +1598,37 @@ var TestRunner = class {
768
1598
  }
769
1599
  return args;
770
1600
  }
1601
+ /**
1602
+ * Conditionally clear the terminal screen
1603
+ *
1604
+ * Clears the terminal screen if the clearScreen option is enabled
1605
+ * in the test runner configuration.
1606
+ */
771
1607
  #clearScreen() {
772
1608
  if (this.options.clearScreen) process.stdout.write("\x1Bc");
773
1609
  }
1610
+ /**
1611
+ * Runs tests as a child process
1612
+ *
1613
+ * Creates a Node.js child process to execute the test script with
1614
+ * appropriate command-line arguments and environment variables.
1615
+ * Handles process lifecycle and hook execution.
1616
+ *
1617
+ * @param port - The port number to set in the environment
1618
+ * @param filters - Optional test filters to apply for this run
1619
+ */
774
1620
  async #runTests(port, hmrPort, filters) {
1621
+ /**
1622
+ * Execute the registered before creating the child process. This will allow
1623
+ * hooks to modify the options before they are used.
1624
+ */
775
1625
  await this.#hooks.runner("testsStarting").run(this);
776
1626
  debug_default("running tests using \"%s\" file, options %O", this.scriptFile, this.options);
777
1627
  return new Promise(async (resolve) => {
1628
+ /**
1629
+ * If inline filters are defined, then we ignore the
1630
+ * initial filters
1631
+ */
778
1632
  const mergedFilters = {
779
1633
  ...this.options.filters,
780
1634
  ...filters
@@ -813,6 +1667,16 @@ var TestRunner = class {
813
1667
  }).finally(() => resolve());
814
1668
  });
815
1669
  }
1670
+ /**
1671
+ * Handles file change event during watch mode
1672
+ *
1673
+ * Determines whether to run specific tests or all tests based on
1674
+ * the type of file that changed. Test files trigger selective runs,
1675
+ * while other files trigger full test suite runs.
1676
+ *
1677
+ * @param filePath - The path of the changed file
1678
+ * @param action - The type of change (add, update, delete)
1679
+ */
816
1680
  #handleFileChange(relativePath, absolutePath, action) {
817
1681
  const file = this.#fileSystem.inspect(absolutePath, relativePath);
818
1682
  if (!file) return;
@@ -821,10 +1685,20 @@ var TestRunner = class {
821
1685
  if (file.fileType === "test") this.#reRunTests({ files: [relativePath] });
822
1686
  else this.#reRunTests();
823
1687
  }
1688
+ /**
1689
+ * Re-generates the index when a file is changed, but only in HMR
1690
+ * mode
1691
+ */
824
1692
  #regenerateIndex(filePath, action) {
825
1693
  if (action === "add") return this.#indexGenerator.addFile(filePath);
826
1694
  return this.#indexGenerator.removeFile(filePath);
827
1695
  }
1696
+ /**
1697
+ * Registers inline hooks for file changes and test re-runs
1698
+ *
1699
+ * Sets up event handlers that respond to file system changes by
1700
+ * triggering appropriate test runs based on the changed files.
1701
+ */
828
1702
  #registerServerRestartHooks() {
829
1703
  this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
830
1704
  this.#regenerateIndex(absolutePath, "add");
@@ -839,14 +1713,32 @@ var TestRunner = class {
839
1713
  this.#handleFileChange(relativePath, absolutePath, "delete");
840
1714
  });
841
1715
  }
1716
+ /**
1717
+ * Add listener to get notified when test runner is closed
1718
+ *
1719
+ * @param callback - Function to call when test runner closes
1720
+ * @returns This TestRunner instance for method chaining
1721
+ */
842
1722
  onClose(callback) {
843
1723
  this.#onClose = callback;
844
1724
  return this;
845
1725
  }
1726
+ /**
1727
+ * Add listener to get notified when test runner encounters an error
1728
+ *
1729
+ * @param callback - Function to call when test runner encounters an error
1730
+ * @returns This TestRunner instance for method chaining
1731
+ */
846
1732
  onError(callback) {
847
1733
  this.#onError = callback;
848
1734
  return this;
849
1735
  }
1736
+ /**
1737
+ * Close watchers and running child processes
1738
+ *
1739
+ * Cleans up file system watchers and terminates any running test
1740
+ * processes to ensure graceful shutdown.
1741
+ */
850
1742
  async close() {
851
1743
  await this.#watcher?.close();
852
1744
  if (this.#testsProcess) {
@@ -854,6 +1746,12 @@ var TestRunner = class {
854
1746
  this.#testsProcess.kill("SIGKILL");
855
1747
  }
856
1748
  }
1749
+ /**
1750
+ * Runs tests once without watching for file changes
1751
+ *
1752
+ * Executes the test suite a single time and exits. This is the
1753
+ * equivalent of running tests in CI/CD environments.
1754
+ */
857
1755
  async run() {
858
1756
  this.#stickyPort = String(await getPort(this.cwd));
859
1757
  this.#stickyHmrPort = String(await getRandomPort({ port: 24678 }));
@@ -865,6 +1763,10 @@ var TestRunner = class {
865
1763
  "testsStarting",
866
1764
  "testsFinished"
867
1765
  ]);
1766
+ /**
1767
+ * Run init hooks and clear them as they won't be executed
1768
+ * ever again
1769
+ */
868
1770
  await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
869
1771
  this.#hooks.clear("init");
870
1772
  this.ui.logger.info("generating indexes...");
@@ -872,6 +1774,17 @@ var TestRunner = class {
872
1774
  this.ui.logger.info("booting application to run tests...");
873
1775
  await this.#runTests(this.#stickyPort, this.#stickyHmrPort);
874
1776
  }
1777
+ /**
1778
+ * Run tests in watch mode and re-run them when files change
1779
+ *
1780
+ * Starts the test runner in watch mode, monitoring the file system
1781
+ * for changes and automatically re-running tests when relevant files
1782
+ * are modified. Uses intelligent filtering to run only affected tests
1783
+ * when possible.
1784
+ *
1785
+ * @param ts - TypeScript module reference for parsing configuration
1786
+ * @param options - Watch options including polling mode for file system monitoring
1787
+ */
875
1788
  async runAndWatch(options) {
876
1789
  const tsConfig = readTsConfig(this.cwdPath);
877
1790
  if (!tsConfig) {
@@ -899,12 +1812,19 @@ var TestRunner = class {
899
1812
  "fileRemoved"
900
1813
  ]);
901
1814
  this.#registerServerRestartHooks();
1815
+ /**
1816
+ * Run init hooks and clear them as they won't be executed
1817
+ * ever again
1818
+ */
902
1819
  await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
903
1820
  this.#hooks.clear("init");
904
1821
  this.ui.logger.info("generating indexes...");
905
1822
  await this.#indexGenerator.generate();
906
1823
  this.ui.logger.info("booting application to run tests...");
907
1824
  await this.#runTests(this.#stickyPort, this.#stickyHmrPort);
1825
+ /**
1826
+ * Create watcher
1827
+ */
908
1828
  this.#watcher = watch({
909
1829
  usePolling: options?.poll ?? false,
910
1830
  cwd: this.cwdPath,
@@ -915,9 +1835,15 @@ var TestRunner = class {
915
1835
  return !this.#fileSystem.shouldWatchDirectory(file);
916
1836
  }
917
1837
  });
1838
+ /**
1839
+ * Notify the watcher is ready
1840
+ */
918
1841
  this.#watcher.on("ready", () => {
919
1842
  this.ui.logger.info("watching file system for changes...");
920
1843
  });
1844
+ /**
1845
+ * Cleanup when watcher dies
1846
+ */
921
1847
  this.#watcher.on("error", (error) => {
922
1848
  this.ui.logger.warning("file system watcher failure");
923
1849
  this.ui.logger.fatal(error);
@@ -945,4 +1871,5 @@ var TestRunner = class {
945
1871
  });
946
1872
  }
947
1873
  };
1874
+ //#endregion
948
1875
  export { Bundler, CodemodException, DevServer, FileBuffer, SUPPORTED_PACKAGE_MANAGERS, TestRunner, VirtualFileSystem };