@adonisjs/assembler 8.0.0-next.4 → 8.0.0-next.6

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 (35) hide show
  1. package/README.md +87 -59
  2. package/build/chunk-F4RAGKQN.js +387 -0
  3. package/build/chunk-MC3FJR62.js +411 -0
  4. package/build/chunk-TIKQQRMX.js +116 -0
  5. package/build/index.d.ts +1 -0
  6. package/build/index.js +606 -410
  7. package/build/src/bundler.d.ts +40 -3
  8. package/build/src/code_scanners/routes_scanner/main.d.ts +49 -9
  9. package/build/src/code_scanners/routes_scanner/main.js +445 -0
  10. package/build/src/code_scanners/routes_scanner/validator_extractor.d.ts +12 -4
  11. package/build/src/code_transformer/main.d.ts +44 -43
  12. package/build/src/code_transformer/main.js +123 -101
  13. package/build/src/code_transformer/rc_file_transformer.d.ts +56 -4
  14. package/build/src/debug.d.ts +12 -0
  15. package/build/src/dev_server.d.ts +40 -30
  16. package/build/src/file_buffer.d.ts +67 -0
  17. package/build/src/file_system.d.ts +45 -7
  18. package/build/src/helpers.d.ts +79 -4
  19. package/build/src/helpers.js +16 -0
  20. package/build/src/index_generator/main.d.ts +73 -0
  21. package/build/src/index_generator/main.js +7 -0
  22. package/build/src/index_generator/source.d.ts +66 -0
  23. package/build/src/paths_resolver.d.ts +27 -2
  24. package/build/src/shortcuts_manager.d.ts +42 -4
  25. package/build/src/test_runner.d.ts +58 -31
  26. package/build/src/types/code_scanners.d.ts +138 -24
  27. package/build/src/types/code_transformer.d.ts +61 -19
  28. package/build/src/types/common.d.ts +199 -55
  29. package/build/src/types/hooks.d.ts +173 -17
  30. package/build/src/types/main.d.ts +13 -0
  31. package/build/src/utils.d.ts +88 -10
  32. package/build/src/virtual_file_system.d.ts +112 -0
  33. package/package.json +9 -3
  34. package/build/chunk-RR4HCA4M.js +0 -7
  35. package/build/src/ast_file_system.d.ts +0 -17
package/build/index.js CHANGED
@@ -1,183 +1,27 @@
1
1
  import {
2
- debug_default
3
- } from "./chunk-RR4HCA4M.js";
2
+ IndexGenerator
3
+ } from "./chunk-MC3FJR62.js";
4
+ import {
5
+ copyFiles,
6
+ debug_default,
7
+ getPort,
8
+ loadHooks,
9
+ memoize,
10
+ parseConfig,
11
+ run,
12
+ runNode,
13
+ throttle,
14
+ watch
15
+ } from "./chunk-F4RAGKQN.js";
4
16
 
5
17
  // src/bundler.ts
6
18
  import dedent from "dedent";
7
19
  import fs from "fs/promises";
8
20
  import { cliui } from "@poppinss/cliui";
9
- import { fileURLToPath as fileURLToPath2 } from "url";
10
- import { join as join2, relative as relative2 } from "path";
21
+ import { fileURLToPath } from "url";
22
+ import { join, relative } from "path";
11
23
  import string from "@poppinss/utils/string";
12
24
  import { detectPackageManager } from "@antfu/install-pkg";
13
-
14
- // src/utils.ts
15
- import Cache from "tmp-cache";
16
- import { isJunk } from "junk";
17
- import fastGlob from "fast-glob";
18
- import Hooks from "@poppinss/hooks";
19
- import { existsSync } from "fs";
20
- import getRandomPort from "get-port";
21
- import { fileURLToPath } from "url";
22
- import { execaNode, execa } from "execa";
23
- import { importDefault } from "@poppinss/utils";
24
- import { copyFile, mkdir } from "fs/promises";
25
- import { EnvLoader, EnvParser } from "@adonisjs/env";
26
- import chokidar from "chokidar";
27
- import { basename, dirname, isAbsolute, join, relative } from "path";
28
- var DEFAULT_NODE_ARGS = ["--import=@poppinss/ts-exec", "--enable-source-maps"];
29
- function parseConfig(cwd, ts) {
30
- const cwdPath = typeof cwd === "string" ? cwd : fileURLToPath(cwd);
31
- const configFile = join(cwdPath, "tsconfig.json");
32
- debug_default('parsing config file "%s"', configFile);
33
- let hardException = null;
34
- const parsedConfig = ts.getParsedCommandLineOfConfigFile(
35
- configFile,
36
- {},
37
- {
38
- ...ts.sys,
39
- useCaseSensitiveFileNames: true,
40
- getCurrentDirectory: () => cwdPath,
41
- onUnRecoverableConfigFileDiagnostic: (error) => hardException = error
42
- }
43
- );
44
- if (hardException) {
45
- const compilerHost = ts.createCompilerHost({});
46
- console.log(ts.formatDiagnosticsWithColorAndContext([hardException], compilerHost));
47
- return;
48
- }
49
- if (parsedConfig.errors.length) {
50
- const compilerHost = ts.createCompilerHost({});
51
- console.log(ts.formatDiagnosticsWithColorAndContext(parsedConfig.errors, compilerHost));
52
- return;
53
- }
54
- return parsedConfig;
55
- }
56
- function runNode(cwd, options) {
57
- const childProcess = execaNode(options.script, options.scriptArgs, {
58
- nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs),
59
- preferLocal: true,
60
- windowsHide: false,
61
- localDir: cwd,
62
- cwd,
63
- reject: options.reject ?? false,
64
- buffer: false,
65
- stdio: options.stdio || "inherit",
66
- env: {
67
- ...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
68
- ...options.env
69
- }
70
- });
71
- return childProcess;
72
- }
73
- function run(cwd, options) {
74
- const childProcess = execa(options.script, options.scriptArgs, {
75
- preferLocal: true,
76
- windowsHide: false,
77
- localDir: cwd,
78
- cwd,
79
- buffer: false,
80
- stdio: options.stdio || "inherit",
81
- env: {
82
- ...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
83
- ...options.env
84
- }
85
- });
86
- return childProcess;
87
- }
88
- function watch(options) {
89
- return chokidar.watch(["."], options);
90
- }
91
- async function getPort(cwd) {
92
- if (process.env.PORT) {
93
- return getRandomPort({ port: Number(process.env.PORT) });
94
- }
95
- const files = await new EnvLoader(cwd).load();
96
- for (let file of files) {
97
- const envVariables = await new EnvParser(file.contents).parse();
98
- if (envVariables.PORT) {
99
- return getRandomPort({ port: Number(envVariables.PORT) });
100
- }
101
- }
102
- return getRandomPort({ port: 3333 });
103
- }
104
- async function copyFiles(files, cwd, outDir) {
105
- const { paths, patterns } = files.reduce(
106
- (result, file) => {
107
- if (fastGlob.isDynamicPattern(file)) {
108
- result.patterns.push(file);
109
- return result;
110
- }
111
- if (existsSync(join(cwd, file))) {
112
- result.paths.push(file);
113
- }
114
- return result;
115
- },
116
- { patterns: [], paths: [] }
117
- );
118
- debug_default("copyFiles inputs: %O, paths: %O, patterns: %O", files, paths, patterns);
119
- const filePaths = paths.concat(await fastGlob(patterns, { cwd, dot: true })).filter((file) => {
120
- return !isJunk(basename(file));
121
- });
122
- debug_default('copying files %O to destination "%s"', filePaths, outDir);
123
- const copyPromises = filePaths.map(async (file) => {
124
- const src = isAbsolute(file) ? file : join(cwd, file);
125
- const dest = join(outDir, relative(cwd, src));
126
- await mkdir(dirname(dest), { recursive: true });
127
- return copyFile(src, dest);
128
- });
129
- return await Promise.all(copyPromises);
130
- }
131
- function memoize(fn, maxKeys) {
132
- const cache = new Cache({ max: maxKeys });
133
- return (input) => {
134
- if (cache.has(input)) {
135
- return cache.get(input);
136
- }
137
- return fn(input);
138
- };
139
- }
140
- async function loadHooks(rcFileHooks, names) {
141
- const groups = names.map((name) => {
142
- return {
143
- group: name,
144
- hooks: rcFileHooks?.[name] ?? []
145
- };
146
- });
147
- const hooks = new Hooks();
148
- for (const { group, hooks: collection } of groups) {
149
- for (const item of collection) {
150
- hooks.add(group, await importDefault(item));
151
- }
152
- }
153
- return hooks;
154
- }
155
- function throttle(fn, name) {
156
- name = name || "throttled";
157
- let isBusy = false;
158
- let hasQueuedCalls = false;
159
- let lastCallArgs;
160
- async function throttled(...args) {
161
- if (isBusy) {
162
- debug_default('ignoring "%s" invocation as current execution is in progress', name);
163
- hasQueuedCalls = true;
164
- lastCallArgs = args;
165
- return;
166
- }
167
- isBusy = true;
168
- debug_default('executing "%s" function', name);
169
- await fn(...args);
170
- isBusy = false;
171
- if (hasQueuedCalls) {
172
- hasQueuedCalls = false;
173
- debug_default('resuming and running latest "%s" invocation', name);
174
- await throttled(...lastCallArgs);
175
- }
176
- }
177
- return throttled;
178
- }
179
-
180
- // src/bundler.ts
181
25
  var SUPPORTED_PACKAGE_MANAGERS = {
182
26
  "npm": {
183
27
  packageManagerFiles: ["package-lock.json"],
@@ -201,25 +45,49 @@ var SUPPORTED_PACKAGE_MANAGERS = {
201
45
  }
202
46
  };
203
47
  var Bundler = class {
204
- constructor(cwd, ts, options) {
205
- this.cwd = cwd;
206
- this.options = options;
207
- this.#cwdPath = fileURLToPath2(this.cwd);
208
- this.#ts = ts;
209
- }
48
+ /**
49
+ * The current working project directory path as string
50
+ */
210
51
  #cwdPath;
52
+ /**
53
+ * Reference to the TypeScript module
54
+ */
211
55
  #ts;
212
56
  /**
213
57
  * Hooks to execute custom actions during the build process
214
58
  */
215
59
  #hooks;
60
+ /**
61
+ * CLI UI instance for displaying colorful messages and progress information
62
+ */
216
63
  ui = cliui();
64
+ /**
65
+ * The current working directory URL
66
+ */
67
+ cwd;
68
+ /**
69
+ * Bundler configuration options including hooks and meta files
70
+ */
71
+ options;
72
+ /**
73
+ * Create a new bundler instance
74
+ *
75
+ * @param cwd - The current working directory URL
76
+ * @param ts - TypeScript module reference
77
+ * @param options - Bundler configuration options
78
+ */
79
+ constructor(cwd, ts, options) {
80
+ this.cwd = cwd;
81
+ this.options = options;
82
+ this.#cwdPath = fileURLToPath(this.cwd);
83
+ this.#ts = ts;
84
+ }
217
85
  /**
218
86
  * Returns the relative unix path for an absolute
219
87
  * file path
220
88
  */
221
89
  #getRelativeName(filePath) {
222
- return string.toUnixSlash(relative2(this.#cwdPath, filePath));
90
+ return string.toUnixSlash(relative(this.#cwdPath, filePath));
223
91
  }
224
92
  /**
225
93
  * Cleans up the build directory
@@ -268,7 +136,7 @@ var Bundler = class {
268
136
  * in a production environment.
269
137
  */
270
138
  async #createAceFile(outDir) {
271
- const aceFileLocation = join2(outDir, "ace.js");
139
+ const aceFileLocation = join(outDir, "ace.js");
272
140
  const aceFileContent = dedent(
273
141
  /* JavaScript */
274
142
  `
@@ -286,6 +154,13 @@ var Bundler = class {
286
154
  }
287
155
  /**
288
156
  * Bundles the application to be run in production
157
+ *
158
+ * @param stopOnError - Whether to stop the build process on TypeScript errors
159
+ * @param client - Override the detected package manager
160
+ * @returns Promise that resolves to true if build succeeded, false otherwise
161
+ *
162
+ * @example
163
+ * const success = await bundler.bundle(true, 'npm')
289
164
  */
290
165
  async bundle(stopOnError = true, client) {
291
166
  this.#hooks = await loadHooks(this.options.hooks, ["buildStarting", "buildFinished"]);
@@ -294,7 +169,7 @@ var Bundler = class {
294
169
  if (!config) {
295
170
  return false;
296
171
  }
297
- const outDir = config.options.outDir || fileURLToPath2(new URL("build/", this.cwd));
172
+ const outDir = config.options.outDir || fileURLToPath(new URL("build/", this.cwd));
298
173
  this.ui.logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
299
174
  await this.#cleanupBuildDirectory(outDir);
300
175
  await this.#hooks.runner("buildStarting").run(this);
@@ -331,19 +206,19 @@ var Bundler = class {
331
206
  };
332
207
 
333
208
  // src/dev_server.ts
334
- import { relative as relative4 } from "path";
335
209
  import { cliui as cliui2 } from "@poppinss/cliui";
336
210
  import prettyHrtime from "pretty-hrtime";
337
- import { fileURLToPath as fileURLToPath4 } from "url";
211
+ import { fileURLToPath as fileURLToPath2 } from "url";
338
212
  import string3 from "@poppinss/utils/string";
213
+ import { join as join2, relative as relative3 } from "path/posix";
214
+ import { RuntimeException } from "@poppinss/utils/exception";
339
215
 
340
216
  // src/file_system.ts
341
217
  import picomatch from "picomatch";
342
- import { fileURLToPath as fileURLToPath3 } from "url";
343
- import { join as join3, relative as relative3 } from "path";
218
+ import { relative as relative2 } from "path/posix";
344
219
  import string2 from "@poppinss/utils/string";
345
220
  var DEFAULT_INCLUDES = ["**/*"];
346
- var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**"];
221
+ var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**", ".adonisjs/**"];
347
222
  var DEFAULT_EXCLUDES = ["node_modules/**", "bower_components/**", "jspm_packages/**"];
348
223
  var FileSystem = class {
349
224
  /**
@@ -385,7 +260,7 @@ var FileSystem = class {
385
260
  */
386
261
  #isTestFile;
387
262
  /**
388
- * References to includes and excludes
263
+ * References to includes and excludes glob patterns
389
264
  */
390
265
  #includes;
391
266
  #excludes;
@@ -413,11 +288,25 @@ var FileSystem = class {
413
288
  return this.#excludes;
414
289
  }
415
290
  /**
416
- * Inspect a relative path to find its source in the project
417
- */
418
- inspect = memoize((filePath) => {
419
- const relativePath = filePath;
420
- const absolutePath = string2.toUnixSlash(join3(this.#cwd, relativePath));
291
+ * Inspect a file path to determine its type and properties within the project.
292
+ *
293
+ * This method analyzes a file to categorize it as a script file, test file, or meta file,
294
+ * and determines whether changes to the file should trigger server restarts. Results
295
+ * are memoized for performance optimization.
296
+ *
297
+ * @param absolutePath - The absolute Unix path to the file
298
+ * @param relativePath - The relative Unix path from the project root
299
+ * @returns File inspection result or null if the file should be ignored
300
+ *
301
+ * @example
302
+ * const file = fileSystem.inspect('/project/app/models/user.ts', 'app/models/user.ts')
303
+ * if (file) {
304
+ * console.log(file.fileType) // 'script'
305
+ * console.log(file.reloadServer) // true
306
+ * }
307
+ */
308
+ inspect = memoize((absolutePath, relativePath) => {
309
+ relativePath = relativePath ?? relative2(this.#cwd, absolutePath);
421
310
  if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
422
311
  debug_default('backend project file "%s"', relativePath);
423
312
  const isTestFile = this.#isTestFile(relativePath);
@@ -450,63 +339,73 @@ var FileSystem = class {
450
339
  return null;
451
340
  });
452
341
  /**
453
- * Returns true if the directory should be watched. Chokidar sends
454
- * absolute unix paths to the ignored callback.
342
+ * Determines if a directory should be watched by the file watcher.
343
+ *
344
+ * This method checks if a directory should be monitored for file changes
345
+ * based on the TypeScript configuration includes/excludes patterns.
346
+ * Results are memoized for performance. Chokidar sends absolute Unix paths.
347
+ *
348
+ * Note: Use shouldWatchFile for files and this method for directories only.
455
349
  *
456
- * You must use "shouldWatchFile" method for files and call this method
457
- * for directories only.
350
+ * @param absolutePath - The absolute Unix path to the directory
351
+ * @returns True if the directory should be watched
352
+ *
353
+ * @example
354
+ * const shouldWatch = fileSystem.shouldWatchDirectory('/project/app/controllers')
355
+ * console.log(shouldWatch) // true
458
356
  */
459
357
  shouldWatchDirectory = memoize((absolutePath) => {
460
358
  if (absolutePath === this.#cwd) {
461
359
  debug_default("watching project root");
462
360
  return true;
463
361
  }
464
- const relativePath = string2.toUnixSlash(relative3(this.#cwd, absolutePath));
362
+ const relativePath = relative2(this.#cwd, absolutePath);
465
363
  if (this.#isExcluded(relativePath)) {
466
364
  debug_default('watching "%s"', absolutePath);
467
365
  return false;
468
366
  }
469
367
  return true;
470
368
  });
369
+ /**
370
+ * Create a new FileSystem instance
371
+ *
372
+ * @param cwd - The current working directory URL or string path
373
+ * @param tsConfig - Parsed TypeScript configuration
374
+ * @param rcFile - AdonisJS RC file configuration
375
+ */
471
376
  constructor(cwd, tsConfig, rcFile) {
472
- this.#cwd = string2.toUnixSlash(typeof cwd === "string" ? cwd : fileURLToPath3(cwd));
377
+ this.#cwd = cwd;
473
378
  this.#tsConfig = tsConfig;
474
379
  const files = tsConfig.fileNames;
475
380
  const metaFiles = rcFile.metaFiles ?? [];
476
381
  const testSuites = rcFile.suites ?? [];
477
382
  const outDir = tsConfig.raw.compilerOptions?.outDir;
478
- files.forEach((file) => this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file)));
383
+ for (const file of files) {
384
+ this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file));
385
+ }
479
386
  this.#includes = tsConfig.raw.include || DEFAULT_INCLUDES;
480
387
  this.#excludes = ALWAYS_EXCLUDE.concat(
481
388
  tsConfig.raw.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES)
482
389
  );
483
- this.#isMetaFileWithReloadsEnabled = picomatch(
484
- metaFiles.filter((file) => !!file.reloadServer).map((file) => file.pattern),
485
- {
486
- cwd: this.#cwd
487
- }
488
- );
489
- this.#isMetaFileWithReloadsDisabled = picomatch(
490
- metaFiles.filter((file) => !file.reloadServer).map((file) => file.pattern),
491
- {
492
- cwd: this.#cwd
493
- }
494
- );
495
- this.#isTestFile = picomatch(
496
- testSuites.flatMap((suite) => suite.files),
497
- {
498
- cwd: this.#cwd
390
+ const metaFilesWithReloads = [];
391
+ const metaFilesWithoutReloads = [];
392
+ for (const file of metaFiles) {
393
+ if (file.reloadServer) {
394
+ metaFilesWithReloads.push(file.pattern);
395
+ } else {
396
+ metaFilesWithoutReloads.push(file.pattern);
499
397
  }
500
- );
501
- this.#isIncluded = picomatch(this.#includes, {
502
- cwd: this.#cwd
503
- });
504
- this.#isExcluded = picomatch(this.#excludes, {
505
- cwd: this.#cwd
506
- });
398
+ }
399
+ const testFilePatterns = testSuites.flatMap((suite) => suite.files);
400
+ const picomatcchOptions = { cwd: this.#cwd };
401
+ this.#isMetaFileWithReloadsEnabled = picomatch(metaFilesWithReloads, picomatcchOptions);
402
+ this.#isMetaFileWithReloadsDisabled = picomatch(metaFilesWithoutReloads, picomatcchOptions);
403
+ this.#isTestFile = picomatch(testFilePatterns, picomatcchOptions);
404
+ this.#isIncluded = picomatch(this.#includes, picomatcchOptions);
405
+ this.#isExcluded = picomatch(this.#excludes, picomatcchOptions);
507
406
  debug_default("initiating file system %O", {
508
407
  includes: this.#includes,
509
- excludes: this.#includes,
408
+ excludes: this.#excludes,
510
409
  outDir,
511
410
  files,
512
411
  metaFiles,
@@ -514,11 +413,15 @@ var FileSystem = class {
514
413
  });
515
414
  }
516
415
  /**
517
- * Returns a boolean telling if a file path is a script file or not.
416
+ * Determines if a file path represents a script file based on TypeScript configuration.
518
417
  *
519
- * - Files ending with ".ts", ".tsx" are considered are script files.
520
- * - Files ending with ".js" with "allowJs" option enabled are considered are script files.
521
- * - Files ending with ".json" with "resolveJsonModule" option enabled are considered are script files.
418
+ * Script files are those that can be processed by the TypeScript compiler:
419
+ * - Files ending with ".ts" or ".tsx" (excluding ".d.ts" declaration files)
420
+ * - Files ending with ".js" when "allowJs" option is enabled in tsconfig
421
+ * - Files ending with ".json" when "resolveJsonModule" option is enabled in tsconfig
422
+ *
423
+ * @param relativePath - The relative file path to check
424
+ * @returns True if the file is a script file
522
425
  */
523
426
  #isScriptFile(relativePath) {
524
427
  if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
@@ -533,9 +436,13 @@ var FileSystem = class {
533
436
  return false;
534
437
  }
535
438
  /**
536
- * Check if the file path is part of the backend TypeScript project. We use
537
- * tsconfig "includes", "excludes", and "files" paths to test if the file
538
- * should be considered or not.
439
+ * Checks if a file path is part of the backend TypeScript project.
440
+ *
441
+ * Uses TypeScript configuration "includes", "excludes", and "files" paths
442
+ * to determine if a file should be considered part of the project compilation.
443
+ *
444
+ * @param relativePath - The relative file path to check
445
+ * @returns True if the file is part of the backend project
539
446
  */
540
447
  #isPartOfBackendProject(relativePath) {
541
448
  if (this.#isExcluded(relativePath)) {
@@ -554,18 +461,36 @@ var FileSystem = class {
554
461
  *
555
462
  * You must use "shouldWatchDirectory" method for directories and call
556
463
  * this method for files only.
464
+ *
465
+ * @param absolutePath - The absolute path to the file
466
+ * @returns True if the file should be watched
557
467
  */
558
468
  shouldWatchFile(absolutePath) {
559
- return this.inspect(relative3(this.#cwd, absolutePath)) !== null;
469
+ return this.inspect(absolutePath) !== null;
560
470
  }
561
471
  };
562
472
 
563
473
  // src/shortcuts_manager.ts
564
474
  var ShortcutsManager = class {
475
+ /**
476
+ * Logger instance for displaying messages
477
+ */
565
478
  #logger;
479
+ /**
480
+ * Callback functions for different keyboard shortcuts
481
+ */
566
482
  #callbacks;
483
+ /**
484
+ * The server URL used for opening browser
485
+ */
567
486
  #serverUrl;
487
+ /**
488
+ * Key press event handler function
489
+ */
568
490
  #keyPressHandler;
491
+ /**
492
+ * Available keyboard shortcuts with their handlers
493
+ */
569
494
  #shortcuts = [
570
495
  {
571
496
  key: "r",
@@ -595,18 +520,32 @@ var ShortcutsManager = class {
595
520
  handler: () => this.showHelp()
596
521
  }
597
522
  ];
523
+ /**
524
+ * Create a new ShortcutsManager instance
525
+ *
526
+ * @param options - Configuration options for the shortcuts manager
527
+ */
598
528
  constructor(options) {
599
529
  this.#logger = options.logger;
600
530
  this.#callbacks = options.callbacks;
601
531
  }
602
532
  /**
603
533
  * Set server url for opening in browser
534
+ *
535
+ * This URL will be used when the user presses 'o' to open the
536
+ * development server in their default browser.
537
+ *
538
+ * @param url - The server URL to open when 'o' key is pressed
604
539
  */
605
540
  setServerUrl(url) {
606
541
  this.#serverUrl = url;
607
542
  }
608
543
  /**
609
- * Initialize keyboard shortcuts
544
+ * Initialize keyboard shortcuts by setting up raw mode on stdin
545
+ *
546
+ * This method enables raw mode on stdin to capture individual keypresses
547
+ * and sets up the event listener for handling keyboard input. Only works
548
+ * in TTY environments.
610
549
  */
611
550
  setup() {
612
551
  if (!process.stdin.isTTY) {
@@ -617,7 +556,12 @@ var ShortcutsManager = class {
617
556
  process.stdin.on("data", this.#keyPressHandler);
618
557
  }
619
558
  /**
620
- * Handle key press events
559
+ * Handle key press events and execute corresponding shortcuts
560
+ *
561
+ * Processes individual key presses and matches them against registered
562
+ * shortcuts. Also handles special key combinations like Ctrl+C and Ctrl+D.
563
+ *
564
+ * @param key - The pressed key as a string
621
565
  */
622
566
  #handleKeyPress(key) {
623
567
  if (key === "" || key === "") {
@@ -629,7 +573,10 @@ var ShortcutsManager = class {
629
573
  }
630
574
  }
631
575
  /**
632
- * Handle opening browser
576
+ * Handle opening browser with the configured server URL
577
+ *
578
+ * Uses the 'open' package to launch the default browser and navigate
579
+ * to the development server URL.
633
580
  */
634
581
  async #handleOpenBrowser() {
635
582
  this.#logger.log("");
@@ -638,7 +585,11 @@ var ShortcutsManager = class {
638
585
  open(this.#serverUrl);
639
586
  }
640
587
  /**
641
- * Show available keyboard shortcuts
588
+ * Show available keyboard shortcuts in the console
589
+ *
590
+ * Displays a formatted list of all available keyboard shortcuts
591
+ * and their descriptions to help users understand what actions
592
+ * are available.
642
593
  */
643
594
  showHelp() {
644
595
  this.#logger.log("");
@@ -646,25 +597,58 @@ var ShortcutsManager = class {
646
597
  this.#shortcuts.forEach(({ key, description }) => this.#logger.log(`\xB7 ${key}: ${description}`));
647
598
  }
648
599
  /**
649
- * Cleanup keyboard shortcuts
600
+ * Cleanup keyboard shortcuts and restore terminal state
601
+ *
602
+ * Disables raw mode on stdin, removes event listeners, and restores
603
+ * the terminal to its normal state. Should be called when shutting down
604
+ * the development server.
650
605
  */
651
606
  cleanup() {
652
607
  if (!process.stdin.isTTY) {
653
608
  return;
654
609
  }
655
610
  process.stdin.setRawMode(false);
611
+ process.stdin.pause();
612
+ process.stdin.unref();
656
613
  process.stdin.removeListener("data", this.#keyPressHandler);
657
614
  this.#keyPressHandler = void 0;
658
615
  }
659
616
  };
660
617
 
661
618
  // src/dev_server.ts
662
- var DevServer = class {
663
- constructor(cwd, options) {
664
- this.cwd = cwd;
665
- this.options = options;
666
- this.#cwdPath = fileURLToPath4(this.cwd);
667
- }
619
+ var DevServer = class _DevServer {
620
+ /**
621
+ * Pre-allocated info object for hot-hook change events to avoid repeated object creation
622
+ */
623
+ static #HOT_HOOK_CHANGE_INFO = {
624
+ source: "hot-hook",
625
+ fullReload: false,
626
+ hotReloaded: false
627
+ };
628
+ /**
629
+ * Pre-allocated info object for hot-hook full reload events
630
+ */
631
+ static #HOT_HOOK_FULL_RELOAD_INFO = {
632
+ source: "hot-hook",
633
+ fullReload: true,
634
+ hotReloaded: false
635
+ };
636
+ /**
637
+ * Pre-allocated info object for hot-hook invalidation events
638
+ */
639
+ static #HOT_HOOK_INVALIDATED_INFO = {
640
+ source: "hot-hook",
641
+ fullReload: false,
642
+ hotReloaded: true
643
+ };
644
+ /**
645
+ * Pre-allocated info object for file watcher events
646
+ */
647
+ static #WATCHER_INFO = {
648
+ source: "watcher",
649
+ fullReload: true,
650
+ hotReloaded: false
651
+ };
668
652
  /**
669
653
  * File path computed from the cwd
670
654
  */
@@ -693,7 +677,7 @@ var DevServer = class {
693
677
  */
694
678
  #httpServer;
695
679
  /**
696
- * Keyboard shortcuts manager
680
+ * Keyboard shortcuts manager instance
697
681
  */
698
682
  #shortcutsManager;
699
683
  /**
@@ -701,10 +685,18 @@ var DevServer = class {
701
685
  * using hot-hook
702
686
  */
703
687
  #fileSystem;
688
+ /**
689
+ * Index generator for managing auto-generated index files
690
+ */
691
+ #indexGenerator;
704
692
  /**
705
693
  * Hooks to execute custom actions during the dev server lifecycle
706
694
  */
707
695
  #hooks;
696
+ /**
697
+ * CLI UI instance for displaying colorful messages and progress information
698
+ */
699
+ #ui = cliui2();
708
700
  /**
709
701
  * Restarts the HTTP server and throttle concurrent calls to
710
702
  * ensure we do not end up with a long loop of restarts
@@ -717,7 +709,10 @@ var DevServer = class {
717
709
  await this.#startHTTPServer(this.#stickyPort);
718
710
  }, "restartHTTPServer");
719
711
  /**
720
- * Sets up keyboard shortcuts
712
+ * Sets up keyboard shortcuts for development server interactions
713
+ *
714
+ * Initializes the shortcuts manager with callbacks for restarting the server,
715
+ * clearing the screen, and quitting the application.
721
716
  */
722
717
  #setupKeyboardShortcuts() {
723
718
  this.#shortcutsManager = new ShortcutsManager({
@@ -731,15 +726,27 @@ var DevServer = class {
731
726
  this.#shortcutsManager.setup();
732
727
  }
733
728
  /**
734
- * Cleanup keyboard shortcuts
729
+ * Cleanup keyboard shortcuts and restore terminal state
730
+ *
731
+ * Removes keyboard shortcuts event listeners and restores the terminal
732
+ * to its normal state when shutting down the development server.
735
733
  */
736
734
  #cleanupKeyboardShortcuts() {
737
735
  this.#shortcutsManager?.cleanup();
738
736
  }
739
737
  /**
740
- * CLI UI to log colorful messages
738
+ * CLI UI instance to log colorful messages and progress information
741
739
  */
742
- ui = cliui2();
740
+ get ui() {
741
+ return this.#ui;
742
+ }
743
+ /**
744
+ * CLI UI instance to log colorful messages and progress information
745
+ */
746
+ set ui(ui) {
747
+ this.#ui = ui;
748
+ this.#indexGenerator.setLogger(ui.logger);
749
+ }
743
750
  /**
744
751
  * The mode in which the DevServer is running.
745
752
  */
@@ -751,17 +758,48 @@ var DevServer = class {
751
758
  */
752
759
  scriptFile = "bin/server.ts";
753
760
  /**
754
- * Inspect if child process message is from AdonisJS HTTP server
761
+ * The current working directory URL
762
+ */
763
+ cwd;
764
+ /**
765
+ * Development server configuration options including hooks and environment variables
766
+ */
767
+ options;
768
+ /**
769
+ * Create a new DevServer instance
770
+ *
771
+ * @param cwd - The current working directory URL
772
+ * @param options - Development server configuration options
773
+ */
774
+ constructor(cwd, options) {
775
+ this.cwd = cwd;
776
+ this.options = options;
777
+ this.#cwdPath = string3.toUnixSlash(fileURLToPath2(this.cwd));
778
+ this.#indexGenerator = new IndexGenerator(this.#cwdPath, this.ui.logger);
779
+ }
780
+ /**
781
+ * Type guard to check if child process message is from AdonisJS HTTP server
782
+ *
783
+ * Validates that a message from the child process contains the expected
784
+ * structure indicating the AdonisJS server is ready and listening.
785
+ *
786
+ * @param message - Unknown message from child process
787
+ * @returns True if message is an AdonisJS ready message
755
788
  */
756
789
  #isAdonisJSReadyMessage(message) {
757
790
  return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
758
791
  }
759
792
  /**
760
- * Displays the server info and executes the hooks after the server has been
761
- * started.
793
+ * Displays server information and executes hooks after server startup
794
+ *
795
+ * Shows server URL, mode, startup duration, and help instructions.
796
+ * Also executes the devServerStarted hooks to allow custom post-startup logic.
797
+ *
798
+ * @param message - Server ready message containing port, host, and optional duration
762
799
  */
763
800
  async #postServerReady(message) {
764
801
  const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
802
+ const info = { host, port: message.port };
765
803
  const serverUrl = `http://${host}:${message.port}`;
766
804
  this.#shortcutsManager?.setServerUrl(serverUrl);
767
805
  const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(serverUrl)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
@@ -770,7 +808,7 @@ var DevServer = class {
770
808
  }
771
809
  displayMessage.add(`Press ${this.ui.colors.dim("h")} to show help`);
772
810
  try {
773
- await this.#hooks.runner("devServerStarted").run(this, displayMessage);
811
+ await this.#hooks.runner("devServerStarted").run(this, info, displayMessage);
774
812
  } catch (error) {
775
813
  this.ui.logger.error('One of the "devServerStarted" hooks failed');
776
814
  this.ui.logger.fatal(error);
@@ -778,13 +816,22 @@ var DevServer = class {
778
816
  displayMessage.render();
779
817
  }
780
818
  /**
781
- * Inspect if child process message is coming from hot-hook
819
+ * Type guard to check if child process message is from hot-hook
820
+ *
821
+ * Validates that a message from the child process is a hot-hook notification
822
+ * about file changes, invalidations, or full reloads.
823
+ *
824
+ * @param message - Unknown message from child process
825
+ * @returns True if message is a hot-hook message
782
826
  */
783
827
  #isHotHookMessage(message) {
784
828
  return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
785
829
  }
786
830
  /**
787
- * Conditionally clear the terminal screen
831
+ * Conditionally clears the terminal screen based on configuration
832
+ *
833
+ * Clears the terminal screen if the clearScreen option is enabled,
834
+ * providing a clean view for development output.
788
835
  */
789
836
  #clearScreen() {
790
837
  if (this.options.clearScreen) {
@@ -792,48 +839,119 @@ var DevServer = class {
792
839
  }
793
840
  }
794
841
  /**
795
- * Handles file change event
842
+ * Handles file change events and triggers appropriate server actions
843
+ *
844
+ * Processes file change notifications and determines whether to restart
845
+ * the server, hot reload, or ignore the change based on file type and mode.
846
+ *
847
+ * @param relativePath - Relative path to the changed file
848
+ * @param absolutePath - Absolute path to the changed file
849
+ * @param action - Type of file change (add, update, delete)
850
+ * @param info - Optional information about the change source and reload behavior
796
851
  */
797
- #handleFileChange(filePath, action, info) {
852
+ #handleFileChange(relativePath, absolutePath, action, info) {
798
853
  if ((action === "add" || action === "delete") && this.mode === "hmr") {
799
- debug_default("ignoring add and delete actions in HMR mode %s", filePath);
854
+ debug_default("ignoring add and delete actions in HMR mode %s", relativePath);
800
855
  return;
801
856
  }
802
857
  if (info && info.source === "hot-hook" && info.hotReloaded) {
803
- debug_default("hot reloading %s, info %O", filePath, info);
804
- this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${filePath}`);
858
+ debug_default("hot reloading %s, info %O", relativePath, info);
859
+ this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${relativePath}`);
805
860
  return;
806
861
  }
807
862
  if (info && !info.fullReload) {
808
- debug_default("ignoring full reload", filePath, info);
863
+ debug_default("ignoring full reload", relativePath, info);
809
864
  return;
810
865
  }
811
- const file = this.#fileSystem.inspect(filePath);
866
+ const file = this.#fileSystem.inspect(absolutePath, relativePath);
812
867
  if (!file) {
813
868
  return;
814
869
  }
815
870
  if (file.reloadServer) {
816
871
  this.#clearScreen();
817
- this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
872
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
818
873
  this.#restartHTTPServer();
819
874
  return;
820
875
  }
821
- this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
876
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
877
+ }
878
+ /**
879
+ * Regenerates index files when a file is added or removed
880
+ *
881
+ * Updates the index generator to reflect file system changes by adding
882
+ * or removing files from the generated index files.
883
+ *
884
+ * @param filePath - Absolute path to the file that changed
885
+ * @param action - Whether the file was added or deleted
886
+ */
887
+ #regenerateIndex(filePath, action) {
888
+ if (action === "add") {
889
+ return this.#indexGenerator.addFile(filePath);
890
+ }
891
+ return this.#indexGenerator.removeFile(filePath);
822
892
  }
823
893
  /**
824
- * Registers inline hooks for the file changes and restarts the
825
- * HTTP server when a file gets changed.
894
+ * Registers hooks for file system events and server restart triggers
895
+ *
896
+ * Sets up event handlers that respond to file additions, changes, and removals
897
+ * by regenerating indexes and handling server restarts as needed.
826
898
  */
827
899
  #registerServerRestartHooks() {
828
- this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
900
+ this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
901
+ this.#regenerateIndex(absolutePath, "add");
902
+ this.#handleFileChange(relativePath, absolutePath, "add");
903
+ });
829
904
  this.#hooks.add(
830
905
  "fileChanged",
831
- (filePath, info) => this.#handleFileChange(filePath, "update", info)
906
+ (relativePath, absolutePath, info) => this.#handleFileChange(relativePath, absolutePath, "update", info)
832
907
  );
833
- this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
908
+ this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
909
+ this.#regenerateIndex(absolutePath, "delete");
910
+ this.#handleFileChange(relativePath, absolutePath, "delete");
911
+ });
912
+ }
913
+ /**
914
+ * Initiate the state for DevServer and executes the init hooks
915
+ */
916
+ async #init(ts, mode) {
917
+ const tsConfig = parseConfig(this.cwd, ts);
918
+ if (!tsConfig) {
919
+ this.#onError?.(new RuntimeException("Unable to parse tsconfig file"));
920
+ return false;
921
+ }
922
+ this.#mode = mode;
923
+ this.#clearScreen();
924
+ this.ui.logger.info(`starting server in ${this.#mode} mode...`);
925
+ this.#stickyPort = String(await getPort(this.cwd));
926
+ this.#fileSystem = new FileSystem(this.#cwdPath, tsConfig, this.options);
927
+ this.ui.logger.info("loading hooks...");
928
+ this.#hooks = await loadHooks(this.options.hooks, [
929
+ "init",
930
+ "routesCommitted",
931
+ "routesScanning",
932
+ "routesScanned",
933
+ "devServerStarting",
934
+ "devServerStarted",
935
+ "fileAdded",
936
+ "fileChanged",
937
+ "fileRemoved"
938
+ ]);
939
+ this.#registerServerRestartHooks();
940
+ this.#setupKeyboardShortcuts();
941
+ await this.#hooks.runner("init").run(this, this.#indexGenerator);
942
+ this.#hooks.clear("init");
943
+ this.ui.logger.info("generating indexes...");
944
+ await this.#indexGenerator.generate();
945
+ return true;
834
946
  }
835
947
  /**
836
- * Starts the HTTP server
948
+ * Starts the HTTP server as a child process
949
+ *
950
+ * Creates a new Node.js child process to run the server script with the
951
+ * specified port and configuration. Sets up message handlers for server
952
+ * ready notifications and hot-hook events.
953
+ *
954
+ * @param port - Port number for the server to listen on
837
955
  */
838
956
  async #startHTTPServer(port) {
839
957
  await this.#hooks.runner("devServerStarting").run(this);
@@ -853,45 +971,21 @@ var DevServer = class {
853
971
  resolve();
854
972
  } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
855
973
  debug_default("received hot-hook message %O", message);
974
+ const absolutePath = message.path ? string3.toUnixSlash(message.path) : "";
975
+ const relativePath = relative3(this.#cwdPath, absolutePath);
856
976
  if (message.type === "hot-hook:file-changed") {
857
- switch (message.action) {
858
- case "add":
859
- this.#hooks.runner("fileAdded").run(string3.toUnixSlash(relative4(this.#cwdPath, message.path)), this);
860
- break;
861
- case "change":
862
- this.#hooks.runner("fileChanged").run(
863
- string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
864
- {
865
- source: "hot-hook",
866
- fullReload: false,
867
- hotReloaded: false
868
- },
869
- this
870
- );
871
- break;
872
- case "unlink":
873
- this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(relative4(this.#cwdPath, message.path)), this);
977
+ const { action } = message;
978
+ if (action === "add") {
979
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
980
+ } else if (action === "change") {
981
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_CHANGE_INFO, this);
982
+ } else if (action === "unlink") {
983
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
874
984
  }
875
985
  } else if (message.type === "hot-hook:full-reload") {
876
- this.#hooks.runner("fileChanged").run(
877
- string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
878
- {
879
- source: "hot-hook",
880
- fullReload: true,
881
- hotReloaded: false
882
- },
883
- this
884
- );
986
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_FULL_RELOAD_INFO, this);
885
987
  } else if (message.type === "hot-hook:invalidated") {
886
- this.#hooks.runner("fileChanged").run(
887
- string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
888
- {
889
- source: "hot-hook",
890
- fullReload: false,
891
- hotReloaded: true
892
- },
893
- this
894
- );
988
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_INVALIDATED_INFO, this);
895
989
  }
896
990
  }
897
991
  });
@@ -913,16 +1007,20 @@ var DevServer = class {
913
1007
  });
914
1008
  }
915
1009
  /**
916
- * Add listener to get notified when dev server is
917
- * closed
1010
+ * Add listener to get notified when dev server is closed
1011
+ *
1012
+ * @param callback - Function to call when dev server closes
1013
+ * @returns This DevServer instance for method chaining
918
1014
  */
919
1015
  onClose(callback) {
920
1016
  this.#onClose = callback;
921
1017
  return this;
922
1018
  }
923
1019
  /**
924
- * Add listener to get notified when dev server exists
925
- * with an error
1020
+ * Add listener to get notified when dev server encounters an error
1021
+ *
1022
+ * @param callback - Function to call when dev server encounters an error
1023
+ * @returns This DevServer instance for method chaining
926
1024
  */
927
1025
  onError(callback) {
928
1026
  this.#onError = callback;
@@ -940,26 +1038,17 @@ var DevServer = class {
940
1038
  }
941
1039
  }
942
1040
  /**
943
- * Start the development server
1041
+ * Start the development server in static or HMR mode
1042
+ *
1043
+ * @param ts - TypeScript module reference
944
1044
  */
945
1045
  async start(ts) {
946
- const tsConfig = parseConfig(this.cwd, ts);
947
- if (!tsConfig) {
1046
+ const initiated = await this.#init(ts, this.options.hmr ? "hmr" : "static");
1047
+ if (!initiated) {
948
1048
  return;
949
1049
  }
950
- this.#stickyPort = String(await getPort(this.cwd));
951
- this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
952
- this.#hooks = await loadHooks(this.options.hooks, [
953
- "devServerStarting",
954
- "devServerStarted",
955
- "fileAdded",
956
- "fileChanged",
957
- "fileRemoved"
958
- ]);
959
- this.#registerServerRestartHooks();
960
- if (this.options.hmr) {
961
- this.#mode = "hmr";
962
- this.options.nodeArgs = this.options.nodeArgs.concat("--import=hot-hook/register");
1050
+ if (this.#mode === "hmr") {
1051
+ this.options.nodeArgs.push("--import=hot-hook/register");
963
1052
  this.options.env = {
964
1053
  ...this.options.env,
965
1054
  HOT_HOOK_INCLUDE: this.#fileSystem.includes.join(","),
@@ -967,32 +1056,20 @@ var DevServer = class {
967
1056
  HOT_HOOK_RESTART: (this.options.metaFiles ?? []).filter(({ reloadServer }) => !!reloadServer).map(({ pattern }) => pattern).join(",")
968
1057
  };
969
1058
  }
970
- this.#clearScreen();
971
- this.#setupKeyboardShortcuts();
972
1059
  this.ui.logger.info("starting HTTP server...");
973
1060
  await this.#startHTTPServer(this.#stickyPort);
974
1061
  }
975
1062
  /**
976
- * Start the development server in watch mode
1063
+ * Start the development server in watch mode and restart on file changes
1064
+ *
1065
+ * @param ts - TypeScript module reference
1066
+ * @param options - Watch options including polling mode
977
1067
  */
978
1068
  async startAndWatch(ts, options) {
979
- const tsConfig = parseConfig(this.cwd, ts);
980
- if (!tsConfig) {
1069
+ const initiated = await this.#init(ts, "watch");
1070
+ if (!initiated) {
981
1071
  return;
982
1072
  }
983
- this.#mode = "watch";
984
- this.#stickyPort = String(await getPort(this.cwd));
985
- this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
986
- this.#hooks = await loadHooks(this.options.hooks, [
987
- "devServerStarting",
988
- "devServerStarted",
989
- "fileAdded",
990
- "fileChanged",
991
- "fileRemoved"
992
- ]);
993
- this.#registerServerRestartHooks();
994
- this.#clearScreen();
995
- this.#setupKeyboardShortcuts();
996
1073
  this.ui.logger.info("starting HTTP server...");
997
1074
  await this.#startHTTPServer(this.#stickyPort);
998
1075
  this.#watcher = watch({
@@ -1018,38 +1095,31 @@ var DevServer = class {
1018
1095
  this.#onError?.(error);
1019
1096
  this.#watcher?.close();
1020
1097
  });
1021
- this.#watcher.on(
1022
- "add",
1023
- (filePath) => this.#hooks.runner("fileAdded").run(string3.toUnixSlash(filePath), this)
1024
- );
1025
- this.#watcher.on(
1026
- "change",
1027
- (filePath) => this.#hooks.runner("fileChanged").run(
1028
- string3.toUnixSlash(filePath),
1029
- {
1030
- source: "watcher",
1031
- fullReload: true,
1032
- hotReloaded: false
1033
- },
1034
- this
1035
- )
1036
- );
1037
- this.#watcher.on(
1038
- "unlink",
1039
- (filePath) => this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(filePath), this)
1040
- );
1098
+ this.#watcher.on("add", (filePath) => {
1099
+ const relativePath = string3.toUnixSlash(filePath);
1100
+ const absolutePath = join2(this.#cwdPath, relativePath);
1101
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1102
+ });
1103
+ this.#watcher.on("change", (filePath) => {
1104
+ const relativePath = string3.toUnixSlash(filePath);
1105
+ const absolutePath = join2(this.#cwdPath, relativePath);
1106
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#WATCHER_INFO, this);
1107
+ });
1108
+ this.#watcher.on("unlink", (filePath) => {
1109
+ const relativePath = string3.toUnixSlash(filePath);
1110
+ const absolutePath = join2(this.#cwdPath, relativePath);
1111
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1112
+ });
1041
1113
  }
1042
1114
  };
1043
1115
 
1044
1116
  // src/test_runner.ts
1117
+ import { join as join3 } from "path/posix";
1045
1118
  import { cliui as cliui3 } from "@poppinss/cliui";
1046
- import { fileURLToPath as fileURLToPath5 } from "url";
1119
+ import { fileURLToPath as fileURLToPath3 } from "url";
1047
1120
  import string4 from "@poppinss/utils/string";
1121
+ import { RuntimeException as RuntimeException2 } from "@poppinss/utils/exception";
1048
1122
  var TestRunner = class {
1049
- constructor(cwd, options) {
1050
- this.cwd = cwd;
1051
- this.options = options;
1052
- }
1053
1123
  /**
1054
1124
  * External listeners that are invoked when child process
1055
1125
  * gets an error or closes
@@ -1078,6 +1148,18 @@ var TestRunner = class {
1078
1148
  * Hooks to execute custom actions during the tests runner lifecycle
1079
1149
  */
1080
1150
  #hooks;
1151
+ /**
1152
+ * The current working directory path as a string
1153
+ */
1154
+ #cwdPath;
1155
+ /**
1156
+ * Index generator for managing auto-generated index files
1157
+ */
1158
+ #indexGenerator;
1159
+ /**
1160
+ * CLI UI instance for displaying colorful messages and progress information
1161
+ */
1162
+ #ui = cliui3();
1081
1163
  /**
1082
1164
  * Re-runs the test child process and throttle concurrent calls to
1083
1165
  * ensure we do not end up with a long loop of restarts
@@ -1090,15 +1172,49 @@ var TestRunner = class {
1090
1172
  await this.#runTests(this.#stickyPort, filters);
1091
1173
  }, "reRunTests");
1092
1174
  /**
1093
- * CLI UI to log colorful messages
1175
+ * CLI UI instance to log colorful messages and progress information
1094
1176
  */
1095
- ui = cliui3();
1177
+ get ui() {
1178
+ return this.#ui;
1179
+ }
1180
+ /**
1181
+ * CLI UI instance to log colorful messages and progress information
1182
+ */
1183
+ set ui(ui) {
1184
+ this.#ui = ui;
1185
+ this.#indexGenerator.setLogger(ui.logger);
1186
+ }
1096
1187
  /**
1097
1188
  * The script file to run as a child process
1098
1189
  */
1099
1190
  scriptFile = "bin/test.ts";
1191
+ /**
1192
+ * The current working directory URL
1193
+ */
1194
+ cwd;
1195
+ /**
1196
+ * Test runner configuration options including filters, reporters, and hooks
1197
+ */
1198
+ options;
1199
+ /**
1200
+ * Create a new TestRunner instance
1201
+ *
1202
+ * @param cwd - The current working directory URL
1203
+ * @param options - Test runner configuration options
1204
+ */
1205
+ constructor(cwd, options) {
1206
+ this.cwd = cwd;
1207
+ this.options = options;
1208
+ this.#cwdPath = string4.toUnixSlash(fileURLToPath3(this.cwd));
1209
+ this.#indexGenerator = new IndexGenerator(this.#cwdPath, this.ui.logger);
1210
+ }
1100
1211
  /**
1101
1212
  * Convert test runner options to the CLI args
1213
+ *
1214
+ * Transforms the test runner configuration options into command-line
1215
+ * arguments that can be passed to the test script.
1216
+ *
1217
+ * @returns Array of command-line arguments
1102
1218
  */
1103
1219
  #convertOptionsToArgs() {
1104
1220
  const args = [];
@@ -1120,7 +1236,13 @@ var TestRunner = class {
1120
1236
  return args;
1121
1237
  }
1122
1238
  /**
1123
- * Converts all known filters to CLI args.
1239
+ * Converts all known filters to CLI args
1240
+ *
1241
+ * Transforms test filters (suites, files, groups, tags, tests) into
1242
+ * command-line arguments for the test script.
1243
+ *
1244
+ * @param filters - The test filters to convert
1245
+ * @returns Array of command-line arguments representing the filters
1124
1246
  */
1125
1247
  #convertFiltersToArgs(filters) {
1126
1248
  const args = [];
@@ -1147,6 +1269,9 @@ var TestRunner = class {
1147
1269
  }
1148
1270
  /**
1149
1271
  * Conditionally clear the terminal screen
1272
+ *
1273
+ * Clears the terminal screen if the clearScreen option is enabled
1274
+ * in the test runner configuration.
1150
1275
  */
1151
1276
  #clearScreen() {
1152
1277
  if (this.options.clearScreen) {
@@ -1154,18 +1279,25 @@ var TestRunner = class {
1154
1279
  }
1155
1280
  }
1156
1281
  /**
1157
- * Runs tests
1282
+ * Runs tests as a child process
1283
+ *
1284
+ * Creates a Node.js child process to execute the test script with
1285
+ * appropriate command-line arguments and environment variables.
1286
+ * Handles process lifecycle and hook execution.
1287
+ *
1288
+ * @param port - The port number to set in the environment
1289
+ * @param filters - Optional test filters to apply for this run
1158
1290
  */
1159
1291
  async #runTests(port, filters) {
1160
1292
  await this.#hooks.runner("testsStarting").run(this);
1161
1293
  debug_default('running tests using "%s" file, options %O', this.scriptFile, this.options);
1162
1294
  return new Promise(async (resolve) => {
1163
- const scriptArgs = this.#convertOptionsToArgs().concat(this.options.scriptArgs).concat(
1164
- this.#convertFiltersToArgs({
1165
- ...this.options.filters,
1166
- ...filters
1167
- })
1168
- );
1295
+ const mergedFilters = { ...this.options.filters, ...filters };
1296
+ const scriptArgs = [
1297
+ ...this.#convertOptionsToArgs(),
1298
+ ...this.options.scriptArgs,
1299
+ ...this.#convertFiltersToArgs(mergedFilters)
1300
+ ];
1169
1301
  this.#testsProcess = runNode(this.cwd, {
1170
1302
  script: this.scriptFile,
1171
1303
  reject: true,
@@ -1194,41 +1326,73 @@ var TestRunner = class {
1194
1326
  });
1195
1327
  }
1196
1328
  /**
1197
- * Handles file change event
1329
+ * Handles file change event during watch mode
1330
+ *
1331
+ * Determines whether to run specific tests or all tests based on
1332
+ * the type of file that changed. Test files trigger selective runs,
1333
+ * while other files trigger full test suite runs.
1334
+ *
1335
+ * @param filePath - The path of the changed file
1336
+ * @param action - The type of change (add, update, delete)
1198
1337
  */
1199
- #handleFileChange(filePath, action) {
1200
- const file = this.#fileSystem.inspect(filePath);
1338
+ #handleFileChange(relativePath, absolutePath, action) {
1339
+ const file = this.#fileSystem.inspect(absolutePath, relativePath);
1201
1340
  if (!file) {
1202
1341
  return;
1203
1342
  }
1204
1343
  this.#clearScreen();
1205
- this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
1344
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
1206
1345
  if (file.fileType === "test") {
1207
- this.#reRunTests({ files: [filePath] });
1346
+ this.#reRunTests({ files: [relativePath] });
1208
1347
  } else {
1209
1348
  this.#reRunTests();
1210
1349
  }
1211
1350
  }
1212
1351
  /**
1213
- * Registers inline hooks for the file changes and restarts the
1214
- * HTTP server when a file gets changed.
1352
+ * Re-generates the index when a file is changed, but only in HMR
1353
+ * mode
1354
+ */
1355
+ #regenerateIndex(filePath, action) {
1356
+ if (action === "add") {
1357
+ return this.#indexGenerator.addFile(filePath);
1358
+ }
1359
+ return this.#indexGenerator.removeFile(filePath);
1360
+ }
1361
+ /**
1362
+ * Registers inline hooks for file changes and test re-runs
1363
+ *
1364
+ * Sets up event handlers that respond to file system changes by
1365
+ * triggering appropriate test runs based on the changed files.
1215
1366
  */
1216
1367
  #registerServerRestartHooks() {
1217
- this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
1218
- this.#hooks.add("fileChanged", (filePath) => this.#handleFileChange(filePath, "update"));
1219
- this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
1368
+ this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
1369
+ this.#regenerateIndex(absolutePath, "add");
1370
+ this.#handleFileChange(relativePath, absolutePath, "add");
1371
+ });
1372
+ this.#hooks.add("fileChanged", (relativePath, absolutePath) => {
1373
+ this.#regenerateIndex(absolutePath, "add");
1374
+ this.#handleFileChange(relativePath, absolutePath, "update");
1375
+ });
1376
+ this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
1377
+ this.#regenerateIndex(absolutePath, "delete");
1378
+ this.#handleFileChange(relativePath, absolutePath, "delete");
1379
+ });
1220
1380
  }
1221
1381
  /**
1222
- * Add listener to get notified when dev server is
1223
- * closed
1382
+ * Add listener to get notified when test runner is closed
1383
+ *
1384
+ * @param callback - Function to call when test runner closes
1385
+ * @returns This TestRunner instance for method chaining
1224
1386
  */
1225
1387
  onClose(callback) {
1226
1388
  this.#onClose = callback;
1227
1389
  return this;
1228
1390
  }
1229
1391
  /**
1230
- * Add listener to get notified when dev server exists
1231
- * with an error
1392
+ * Add listener to get notified when test runner encounters an error
1393
+ *
1394
+ * @param callback - Function to call when test runner encounters an error
1395
+ * @returns This TestRunner instance for method chaining
1232
1396
  */
1233
1397
  onError(callback) {
1234
1398
  this.#onError = callback;
@@ -1236,6 +1400,9 @@ var TestRunner = class {
1236
1400
  }
1237
1401
  /**
1238
1402
  * Close watchers and running child processes
1403
+ *
1404
+ * Cleans up file system watchers and terminates any running test
1405
+ * processes to ensure graceful shutdown.
1239
1406
  */
1240
1407
  async close() {
1241
1408
  await this.#watcher?.close();
@@ -1245,31 +1412,49 @@ var TestRunner = class {
1245
1412
  }
1246
1413
  }
1247
1414
  /**
1248
- * Runs tests
1415
+ * Runs tests once without watching for file changes
1416
+ *
1417
+ * Executes the test suite a single time and exits. This is the
1418
+ * equivalent of running tests in CI/CD environments.
1249
1419
  */
1250
1420
  async run() {
1251
1421
  this.#stickyPort = String(await getPort(this.cwd));
1422
+ this.#clearScreen();
1423
+ this.ui.logger.info("loading hooks...");
1252
1424
  this.#hooks = await loadHooks(this.options.hooks, [
1425
+ "init",
1253
1426
  "testsStarting",
1254
1427
  "testsFinished",
1255
1428
  "fileAdded",
1256
1429
  "fileChanged",
1257
1430
  "fileRemoved"
1258
1431
  ]);
1259
- this.#clearScreen();
1432
+ await this.#hooks.runner("init").run(this, this.#indexGenerator);
1433
+ this.#hooks.clear("init");
1434
+ this.ui.logger.info("generating indexes...");
1435
+ await this.#indexGenerator.generate();
1260
1436
  this.ui.logger.info("booting application to run tests...");
1261
1437
  await this.#runTests(this.#stickyPort);
1262
1438
  }
1263
1439
  /**
1264
- * Run tests in watch mode
1440
+ * Run tests in watch mode and re-run them when files change
1441
+ *
1442
+ * Starts the test runner in watch mode, monitoring the file system
1443
+ * for changes and automatically re-running tests when relevant files
1444
+ * are modified. Uses intelligent filtering to run only affected tests
1445
+ * when possible.
1446
+ *
1447
+ * @param ts - TypeScript module reference for parsing configuration
1448
+ * @param options - Watch options including polling mode for file system monitoring
1265
1449
  */
1266
1450
  async runAndWatch(ts, options) {
1267
1451
  const tsConfig = parseConfig(this.cwd, ts);
1268
1452
  if (!tsConfig) {
1453
+ this.#onError?.(new RuntimeException2("Unable to parse tsconfig file"));
1269
1454
  return;
1270
1455
  }
1271
1456
  this.#stickyPort = String(await getPort(this.cwd));
1272
- this.#fileSystem = new FileSystem(this.cwd, tsConfig, {
1457
+ this.#fileSystem = new FileSystem(this.#cwdPath, tsConfig, {
1273
1458
  ...this.options,
1274
1459
  suites: this.options.suites?.filter((suite) => {
1275
1460
  if (this.options.filters.suites) {
@@ -1278,7 +1463,10 @@ var TestRunner = class {
1278
1463
  return true;
1279
1464
  })
1280
1465
  });
1466
+ this.#clearScreen();
1467
+ this.ui.logger.info("loading hooks...");
1281
1468
  this.#hooks = await loadHooks(this.options.hooks, [
1469
+ "init",
1282
1470
  "testsStarting",
1283
1471
  "testsFinished",
1284
1472
  "fileAdded",
@@ -1286,12 +1474,15 @@ var TestRunner = class {
1286
1474
  "fileRemoved"
1287
1475
  ]);
1288
1476
  this.#registerServerRestartHooks();
1289
- this.#clearScreen();
1477
+ await this.#hooks.runner("init").run(this, this.#indexGenerator);
1478
+ this.#hooks.clear("init");
1479
+ this.ui.logger.info("generating indexes...");
1480
+ await this.#indexGenerator.generate();
1290
1481
  this.ui.logger.info("booting application to run tests...");
1291
1482
  await this.#runTests(this.#stickyPort);
1292
1483
  this.#watcher = watch({
1293
1484
  usePolling: options?.poll ?? false,
1294
- cwd: fileURLToPath5(this.cwd),
1485
+ cwd: this.#cwdPath,
1295
1486
  ignoreInitial: true,
1296
1487
  ignored: (file, stats) => {
1297
1488
  if (!stats) {
@@ -1312,30 +1503,35 @@ var TestRunner = class {
1312
1503
  this.#onError?.(error);
1313
1504
  this.#watcher?.close();
1314
1505
  });
1315
- this.#watcher.on(
1316
- "add",
1317
- (filePath) => this.#hooks.runner("fileAdded").run(string4.toUnixSlash(filePath), this)
1318
- );
1319
- this.#watcher.on(
1320
- "change",
1321
- (filePath) => this.#hooks.runner("fileChanged").run(
1322
- string4.toUnixSlash(filePath),
1506
+ this.#watcher.on("add", (filePath) => {
1507
+ const relativePath = string4.toUnixSlash(filePath);
1508
+ const absolutePath = join3(this.#cwdPath, filePath);
1509
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1510
+ });
1511
+ this.#watcher.on("change", (filePath) => {
1512
+ const relativePath = string4.toUnixSlash(filePath);
1513
+ const absolutePath = join3(this.#cwdPath, filePath);
1514
+ this.#hooks.runner("fileChanged").run(
1515
+ relativePath,
1516
+ absolutePath,
1323
1517
  {
1324
1518
  source: "watcher",
1325
1519
  fullReload: true,
1326
1520
  hotReloaded: false
1327
1521
  },
1328
1522
  this
1329
- )
1330
- );
1331
- this.#watcher.on(
1332
- "unlink",
1333
- (filePath) => this.#hooks.runner("fileRemoved").run(string4.toUnixSlash(filePath), this)
1334
- );
1523
+ );
1524
+ });
1525
+ this.#watcher.on("unlink", (filePath) => {
1526
+ const relativePath = string4.toUnixSlash(filePath);
1527
+ const absolutePath = join3(this.#cwdPath, filePath);
1528
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1529
+ });
1335
1530
  }
1336
1531
  };
1337
1532
  export {
1338
1533
  Bundler,
1339
1534
  DevServer,
1535
+ SUPPORTED_PACKAGE_MANAGERS,
1340
1536
  TestRunner
1341
1537
  };