@adonisjs/assembler 8.0.0-next.0 → 8.0.0-next.10

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