@adonisjs/assembler 8.0.0-next.1 → 8.0.0-next.11

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-FSSS6GZ2.js +460 -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 +727 -418
  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 +87 -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 +235 -22
  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 -22
  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-FSSS6GZ2.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);
@@ -331,19 +218,19 @@ var Bundler = class {
331
218
  };
332
219
 
333
220
  // src/dev_server.ts
334
- import { relative as relative4 } from "path";
335
221
  import { cliui as cliui2 } from "@poppinss/cliui";
336
222
  import prettyHrtime from "pretty-hrtime";
337
- import { fileURLToPath as fileURLToPath4 } from "url";
223
+ import { fileURLToPath as fileURLToPath2 } from "url";
338
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";
339
227
 
340
228
  // src/file_system.ts
341
229
  import picomatch from "picomatch";
342
- import { fileURLToPath as fileURLToPath3 } from "url";
343
- import { join as join3, relative as relative3 } from "path";
230
+ import { relative as relative2 } from "path/posix";
344
231
  import string2 from "@poppinss/utils/string";
345
232
  var DEFAULT_INCLUDES = ["**/*"];
346
- var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**"];
233
+ var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**", ".adonisjs/**"];
347
234
  var DEFAULT_EXCLUDES = ["node_modules/**", "bower_components/**", "jspm_packages/**"];
348
235
  var FileSystem = class {
349
236
  /**
@@ -385,7 +272,7 @@ var FileSystem = class {
385
272
  */
386
273
  #isTestFile;
387
274
  /**
388
- * References to includes and excludes
275
+ * References to includes and excludes glob patterns
389
276
  */
390
277
  #includes;
391
278
  #excludes;
@@ -413,11 +300,25 @@ var FileSystem = class {
413
300
  return this.#excludes;
414
301
  }
415
302
  /**
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));
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);
421
322
  if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
422
323
  debug_default('backend project file "%s"', relativePath);
423
324
  const isTestFile = this.#isTestFile(relativePath);
@@ -450,63 +351,73 @@ var FileSystem = class {
450
351
  return null;
451
352
  });
452
353
  /**
453
- * Returns true if the directory should be watched. Chokidar sends
454
- * 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.
361
+ *
362
+ * @param absolutePath - The absolute Unix path to the directory
363
+ * @returns True if the directory should be watched
455
364
  *
456
- * You must use "shouldWatchFile" method for files and call this method
457
- * for directories only.
365
+ * @example
366
+ * const shouldWatch = fileSystem.shouldWatchDirectory('/project/app/controllers')
367
+ * console.log(shouldWatch) // true
458
368
  */
459
369
  shouldWatchDirectory = memoize((absolutePath) => {
460
370
  if (absolutePath === this.#cwd) {
461
371
  debug_default("watching project root");
462
372
  return true;
463
373
  }
464
- const relativePath = string2.toUnixSlash(relative3(this.#cwd, absolutePath));
374
+ const relativePath = relative2(this.#cwd, absolutePath);
465
375
  if (this.#isExcluded(relativePath)) {
466
376
  debug_default('watching "%s"', absolutePath);
467
377
  return false;
468
378
  }
469
379
  return true;
470
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
+ */
471
388
  constructor(cwd, tsConfig, rcFile) {
472
- this.#cwd = string2.toUnixSlash(typeof cwd === "string" ? cwd : fileURLToPath3(cwd));
389
+ this.#cwd = cwd;
473
390
  this.#tsConfig = tsConfig;
474
391
  const files = tsConfig.fileNames;
475
392
  const metaFiles = rcFile.metaFiles ?? [];
476
393
  const testSuites = rcFile.suites ?? [];
477
394
  const outDir = tsConfig.raw.compilerOptions?.outDir;
478
- files.forEach((file) => this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file)));
395
+ for (const file of files) {
396
+ this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file));
397
+ }
479
398
  this.#includes = tsConfig.raw.include || DEFAULT_INCLUDES;
480
399
  this.#excludes = ALWAYS_EXCLUDE.concat(
481
400
  tsConfig.raw.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES)
482
401
  );
483
- this.#isMetaFileWithReloadsEnabled = picomatch(
484
- metaFiles.filter((file) => !!file.reloadServer).map((file) => file.pattern),
485
- {
486
- 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);
487
409
  }
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
499
- }
500
- );
501
- this.#isIncluded = picomatch(this.#includes, {
502
- cwd: this.#cwd
503
- });
504
- this.#isExcluded = picomatch(this.#excludes, {
505
- cwd: this.#cwd
506
- });
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);
507
418
  debug_default("initiating file system %O", {
508
419
  includes: this.#includes,
509
- excludes: this.#includes,
420
+ excludes: this.#excludes,
510
421
  outDir,
511
422
  files,
512
423
  metaFiles,
@@ -514,11 +425,15 @@ var FileSystem = class {
514
425
  });
515
426
  }
516
427
  /**
517
- * 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.
429
+ *
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
518
434
  *
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.
435
+ * @param relativePath - The relative file path to check
436
+ * @returns True if the file is a script file
522
437
  */
523
438
  #isScriptFile(relativePath) {
524
439
  if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
@@ -533,9 +448,13 @@ var FileSystem = class {
533
448
  return false;
534
449
  }
535
450
  /**
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.
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
539
458
  */
540
459
  #isPartOfBackendProject(relativePath) {
541
460
  if (this.#isExcluded(relativePath)) {
@@ -554,23 +473,194 @@ var FileSystem = class {
554
473
  *
555
474
  * You must use "shouldWatchDirectory" method for directories and call
556
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
557
479
  */
558
480
  shouldWatchFile(absolutePath) {
559
- return this.inspect(relative3(this.#cwd, absolutePath)) !== null;
481
+ return this.inspect(absolutePath) !== null;
560
482
  }
561
483
  };
562
484
 
563
- // src/dev_server.ts
564
- var DevServer = class {
565
- constructor(cwd, options) {
566
- this.cwd = cwd;
567
- this.options = options;
568
- this.#cwdPath = fileURLToPath4(this.cwd);
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;
569
543
  }
570
544
  /**
571
- * File path computed from the cwd
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
572
551
  */
573
- #cwdPath;
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
+ };
574
664
  /**
575
665
  * External listeners that are invoked when child process
576
666
  * gets an error or closes
@@ -594,15 +684,27 @@ var DevServer = class {
594
684
  * Reference to the child process
595
685
  */
596
686
  #httpServer;
687
+ /**
688
+ * Keyboard shortcuts manager instance
689
+ */
690
+ #shortcutsManager;
597
691
  /**
598
692
  * Filesystem is used to decide which files to watch or entertain when
599
693
  * using hot-hook
600
694
  */
601
695
  #fileSystem;
696
+ /**
697
+ * Index generator for managing auto-generated index files
698
+ */
699
+ #indexGenerator;
602
700
  /**
603
701
  * Hooks to execute custom actions during the dev server lifecycle
604
702
  */
605
703
  #hooks;
704
+ /**
705
+ * CLI UI instance for displaying colorful messages and progress information
706
+ */
707
+ ui = cliui2();
606
708
  /**
607
709
  * Restarts the HTTP server and throttle concurrent calls to
608
710
  * ensure we do not end up with a long loop of restarts
@@ -615,9 +717,31 @@ var DevServer = class {
615
717
  await this.#startHTTPServer(this.#stickyPort);
616
718
  }, "restartHTTPServer");
617
719
  /**
618
- * 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.
619
741
  */
620
- ui = cliui2();
742
+ #cleanupKeyboardShortcuts() {
743
+ this.#shortcutsManager?.cleanup();
744
+ }
621
745
  /**
622
746
  * The mode in which the DevServer is running.
623
747
  */
@@ -629,23 +753,60 @@ var DevServer = class {
629
753
  */
630
754
  scriptFile = "bin/server.ts";
631
755
  /**
632
- * 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
633
786
  */
634
787
  #isAdonisJSReadyMessage(message) {
635
788
  return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
636
789
  }
637
790
  /**
638
- * Displays the server info and executes the hooks after the server has been
639
- * 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
640
797
  */
641
798
  async #postServerReady(message) {
642
799
  const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
643
- 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)}`);
644
804
  if (message.duration) {
645
805
  displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
646
806
  }
807
+ displayMessage.add(`Press ${this.ui.colors.dim("h")} to show help`);
647
808
  try {
648
- await this.#hooks.runner("devServerStarted").run(this, displayMessage);
809
+ await this.#hooks.runner("devServerStarted").run(this, info, displayMessage);
649
810
  } catch (error) {
650
811
  this.ui.logger.error('One of the "devServerStarted" hooks failed');
651
812
  this.ui.logger.fatal(error);
@@ -653,13 +814,22 @@ var DevServer = class {
653
814
  displayMessage.render();
654
815
  }
655
816
  /**
656
- * 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
657
824
  */
658
825
  #isHotHookMessage(message) {
659
826
  return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
660
827
  }
661
828
  /**
662
- * 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.
663
833
  */
664
834
  #clearScreen() {
665
835
  if (this.options.clearScreen) {
@@ -667,48 +837,120 @@ var DevServer = class {
667
837
  }
668
838
  }
669
839
  /**
670
- * Handles file change event
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
671
849
  */
672
- #handleFileChange(filePath, action, info) {
850
+ #handleFileChange(relativePath, absolutePath, action, info) {
673
851
  if ((action === "add" || action === "delete") && this.mode === "hmr") {
674
- debug_default("ignoring add and delete actions in HMR mode %s", filePath);
852
+ debug_default("ignoring add and delete actions in HMR mode %s", relativePath);
675
853
  return;
676
854
  }
677
855
  if (info && info.source === "hot-hook" && info.hotReloaded) {
678
- debug_default("hot reloading %s, info %O", filePath, info);
679
- this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${filePath}`);
856
+ debug_default("hot reloading %s, info %O", relativePath, info);
857
+ this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${relativePath}`);
680
858
  return;
681
859
  }
682
860
  if (info && !info.fullReload) {
683
- debug_default("ignoring full reload", filePath, info);
861
+ debug_default("ignoring full reload", relativePath, info);
684
862
  return;
685
863
  }
686
- const file = this.#fileSystem.inspect(filePath);
864
+ const file = this.#fileSystem.inspect(absolutePath, relativePath);
687
865
  if (!file) {
688
866
  return;
689
867
  }
690
868
  if (file.reloadServer) {
691
869
  this.#clearScreen();
692
- this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
870
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
693
871
  this.#restartHTTPServer();
694
872
  return;
695
873
  }
696
- this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
874
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
697
875
  }
698
876
  /**
699
- * Registers inline hooks for the file changes and restarts the
700
- * 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.
701
896
  */
702
897
  #registerServerRestartHooks() {
703
- 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
+ });
704
902
  this.#hooks.add(
705
903
  "fileChanged",
706
- (filePath, info) => this.#handleFileChange(filePath, "update", info)
904
+ (relativePath, absolutePath, info) => this.#handleFileChange(relativePath, absolutePath, "update", info)
707
905
  );
708
- 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
+ });
709
910
  }
710
911
  /**
711
- * 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
712
954
  */
713
955
  async #startHTTPServer(port) {
714
956
  await this.#hooks.runner("devServerStarting").run(this);
@@ -728,45 +970,21 @@ var DevServer = class {
728
970
  resolve();
729
971
  } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
730
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);
731
975
  if (message.type === "hot-hook:file-changed") {
732
- switch (message.action) {
733
- case "add":
734
- this.#hooks.runner("fileAdded").run(string3.toUnixSlash(relative4(this.#cwdPath, message.path)), this);
735
- break;
736
- case "change":
737
- this.#hooks.runner("fileChanged").run(
738
- string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
739
- {
740
- source: "hot-hook",
741
- fullReload: false,
742
- hotReloaded: false
743
- },
744
- this
745
- );
746
- break;
747
- case "unlink":
748
- this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(relative4(this.#cwdPath, 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);
749
983
  }
750
984
  } else if (message.type === "hot-hook:full-reload") {
751
- this.#hooks.runner("fileChanged").run(
752
- string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
753
- {
754
- source: "hot-hook",
755
- fullReload: true,
756
- hotReloaded: false
757
- },
758
- this
759
- );
985
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_FULL_RELOAD_INFO, this);
760
986
  } else if (message.type === "hot-hook:invalidated") {
761
- this.#hooks.runner("fileChanged").run(
762
- string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
763
- {
764
- source: "hot-hook",
765
- fullReload: false,
766
- hotReloaded: true
767
- },
768
- this
769
- );
987
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_INVALIDATED_INFO, this);
770
988
  }
771
989
  }
772
990
  });
@@ -788,16 +1006,20 @@ var DevServer = class {
788
1006
  });
789
1007
  }
790
1008
  /**
791
- * Add listener to get notified when dev server is
792
- * 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
793
1013
  */
794
1014
  onClose(callback) {
795
1015
  this.#onClose = callback;
796
1016
  return this;
797
1017
  }
798
1018
  /**
799
- * Add listener to get notified when dev server exists
800
- * 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
801
1023
  */
802
1024
  onError(callback) {
803
1025
  this.#onError = callback;
@@ -807,6 +1029,7 @@ var DevServer = class {
807
1029
  * Close watchers and the running child process
808
1030
  */
809
1031
  async close() {
1032
+ this.#cleanupKeyboardShortcuts();
810
1033
  await this.#watcher?.close();
811
1034
  if (this.#httpServer) {
812
1035
  this.#httpServer.removeAllListeners();
@@ -814,67 +1037,51 @@ var DevServer = class {
814
1037
  }
815
1038
  }
816
1039
  /**
817
- * Start the development server
1040
+ * Start the development server in static or HMR mode
1041
+ *
1042
+ * @param ts - TypeScript module reference
818
1043
  */
819
1044
  async start(ts) {
820
- const tsConfig = parseConfig(this.cwd, ts);
821
- if (!tsConfig) {
1045
+ const initiated = await this.#init(ts, this.options.hmr ? "hmr" : "static");
1046
+ if (!initiated) {
822
1047
  return;
823
1048
  }
824
- this.#stickyPort = String(await getPort(this.cwd));
825
- this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
826
- this.#hooks = await loadHooks(this.options.hooks, [
827
- "devServerStarting",
828
- "devServerStarted",
829
- "fileAdded",
830
- "fileChanged",
831
- "fileRemoved"
832
- ]);
833
- this.#registerServerRestartHooks();
834
- if (this.options.hmr) {
835
- this.#mode = "hmr";
836
- 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");
837
1051
  this.options.env = {
838
1052
  ...this.options.env,
839
1053
  HOT_HOOK_INCLUDE: this.#fileSystem.includes.join(","),
840
- HOT_HOOK_IGNORE: this.#fileSystem.excludes.join(","),
1054
+ HOT_HOOK_IGNORE: this.#fileSystem.excludes.filter((exclude) => !exclude.includes("inertia")).join(","),
841
1055
  HOT_HOOK_RESTART: (this.options.metaFiles ?? []).filter(({ reloadServer }) => !!reloadServer).map(({ pattern }) => pattern).join(",")
842
1056
  };
843
1057
  }
844
- this.#clearScreen();
845
1058
  this.ui.logger.info("starting HTTP server...");
846
1059
  await this.#startHTTPServer(this.#stickyPort);
847
1060
  }
848
1061
  /**
849
- * 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
850
1066
  */
851
1067
  async startAndWatch(ts, options) {
852
- const tsConfig = parseConfig(this.cwd, ts);
853
- if (!tsConfig) {
1068
+ const initiated = await this.#init(ts, "watch");
1069
+ if (!initiated) {
854
1070
  return;
855
1071
  }
856
- this.#mode = "watch";
857
- this.#stickyPort = String(await getPort(this.cwd));
858
- this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
859
- this.#hooks = await loadHooks(this.options.hooks, [
860
- "devServerStarting",
861
- "devServerStarted",
862
- "fileAdded",
863
- "fileChanged",
864
- "fileRemoved"
865
- ]);
866
- this.#registerServerRestartHooks();
867
- this.#clearScreen();
868
1072
  this.ui.logger.info("starting HTTP server...");
869
1073
  await this.#startHTTPServer(this.#stickyPort);
870
1074
  this.#watcher = watch({
871
1075
  usePolling: options?.poll ?? false,
872
- cwd: this.#cwdPath,
1076
+ cwd: this.cwdPath,
873
1077
  ignoreInitial: true,
874
1078
  ignored: (file, stats) => {
875
1079
  if (!stats) {
876
1080
  return false;
877
1081
  }
1082
+ if (file.includes("inertia") && !file.includes("node_modules")) {
1083
+ return false;
1084
+ }
878
1085
  if (stats.isFile()) {
879
1086
  return !this.#fileSystem.shouldWatchFile(file);
880
1087
  }
@@ -890,38 +1097,31 @@ var DevServer = class {
890
1097
  this.#onError?.(error);
891
1098
  this.#watcher?.close();
892
1099
  });
893
- this.#watcher.on(
894
- "add",
895
- (filePath) => this.#hooks.runner("fileAdded").run(string3.toUnixSlash(filePath), this)
896
- );
897
- this.#watcher.on(
898
- "change",
899
- (filePath) => this.#hooks.runner("fileChanged").run(
900
- string3.toUnixSlash(filePath),
901
- {
902
- source: "watcher",
903
- fullReload: true,
904
- hotReloaded: false
905
- },
906
- this
907
- )
908
- );
909
- this.#watcher.on(
910
- "unlink",
911
- (filePath) => this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(filePath), this)
912
- );
1100
+ this.#watcher.on("add", (filePath) => {
1101
+ const relativePath = string3.toUnixSlash(filePath);
1102
+ const absolutePath = join2(this.cwdPath, relativePath);
1103
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1104
+ });
1105
+ this.#watcher.on("change", (filePath) => {
1106
+ const relativePath = string3.toUnixSlash(filePath);
1107
+ const absolutePath = join2(this.cwdPath, relativePath);
1108
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#WATCHER_INFO, this);
1109
+ });
1110
+ this.#watcher.on("unlink", (filePath) => {
1111
+ const relativePath = string3.toUnixSlash(filePath);
1112
+ const absolutePath = join2(this.cwdPath, relativePath);
1113
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1114
+ });
913
1115
  }
914
1116
  };
915
1117
 
916
1118
  // src/test_runner.ts
1119
+ import { join as join3 } from "path/posix";
917
1120
  import { cliui as cliui3 } from "@poppinss/cliui";
918
- import { fileURLToPath as fileURLToPath5 } from "url";
1121
+ import { fileURLToPath as fileURLToPath3 } from "url";
919
1122
  import string4 from "@poppinss/utils/string";
1123
+ import { RuntimeException as RuntimeException2 } from "@poppinss/utils/exception";
920
1124
  var TestRunner = class {
921
- constructor(cwd, options) {
922
- this.cwd = cwd;
923
- this.options = options;
924
- }
925
1125
  /**
926
1126
  * External listeners that are invoked when child process
927
1127
  * gets an error or closes
@@ -950,6 +1150,14 @@ var TestRunner = class {
950
1150
  * Hooks to execute custom actions during the tests runner lifecycle
951
1151
  */
952
1152
  #hooks;
1153
+ /**
1154
+ * Index generator for managing auto-generated index files
1155
+ */
1156
+ #indexGenerator;
1157
+ /**
1158
+ * CLI UI instance for displaying colorful messages and progress information
1159
+ */
1160
+ ui = cliui3();
953
1161
  /**
954
1162
  * Re-runs the test child process and throttle concurrent calls to
955
1163
  * ensure we do not end up with a long loop of restarts
@@ -961,16 +1169,40 @@ var TestRunner = class {
961
1169
  }
962
1170
  await this.#runTests(this.#stickyPort, filters);
963
1171
  }, "reRunTests");
964
- /**
965
- * CLI UI to log colorful messages
966
- */
967
- ui = cliui3();
968
1172
  /**
969
1173
  * The script file to run as a child process
970
1174
  */
971
1175
  scriptFile = "bin/test.ts";
1176
+ /**
1177
+ * The current working directory URL
1178
+ */
1179
+ cwd;
1180
+ /**
1181
+ * The current working directory path as a string
1182
+ */
1183
+ cwdPath;
1184
+ /**
1185
+ * Test runner configuration options including filters, reporters, and hooks
1186
+ */
1187
+ options;
1188
+ /**
1189
+ * Create a new TestRunner instance
1190
+ *
1191
+ * @param cwd - The current working directory URL
1192
+ * @param options - Test runner configuration options
1193
+ */
1194
+ constructor(cwd, options) {
1195
+ this.cwd = cwd;
1196
+ this.options = options;
1197
+ this.cwdPath = string4.toUnixSlash(fileURLToPath3(this.cwd));
1198
+ }
972
1199
  /**
973
1200
  * Convert test runner options to the CLI args
1201
+ *
1202
+ * Transforms the test runner configuration options into command-line
1203
+ * arguments that can be passed to the test script.
1204
+ *
1205
+ * @returns Array of command-line arguments
974
1206
  */
975
1207
  #convertOptionsToArgs() {
976
1208
  const args = [];
@@ -992,7 +1224,13 @@ var TestRunner = class {
992
1224
  return args;
993
1225
  }
994
1226
  /**
995
- * Converts all known filters to CLI args.
1227
+ * Converts all known filters to CLI args
1228
+ *
1229
+ * Transforms test filters (suites, files, groups, tags, tests) into
1230
+ * command-line arguments for the test script.
1231
+ *
1232
+ * @param filters - The test filters to convert
1233
+ * @returns Array of command-line arguments representing the filters
996
1234
  */
997
1235
  #convertFiltersToArgs(filters) {
998
1236
  const args = [];
@@ -1019,6 +1257,9 @@ var TestRunner = class {
1019
1257
  }
1020
1258
  /**
1021
1259
  * Conditionally clear the terminal screen
1260
+ *
1261
+ * Clears the terminal screen if the clearScreen option is enabled
1262
+ * in the test runner configuration.
1022
1263
  */
1023
1264
  #clearScreen() {
1024
1265
  if (this.options.clearScreen) {
@@ -1026,18 +1267,25 @@ var TestRunner = class {
1026
1267
  }
1027
1268
  }
1028
1269
  /**
1029
- * Runs tests
1270
+ * Runs tests as a child process
1271
+ *
1272
+ * Creates a Node.js child process to execute the test script with
1273
+ * appropriate command-line arguments and environment variables.
1274
+ * Handles process lifecycle and hook execution.
1275
+ *
1276
+ * @param port - The port number to set in the environment
1277
+ * @param filters - Optional test filters to apply for this run
1030
1278
  */
1031
1279
  async #runTests(port, filters) {
1032
1280
  await this.#hooks.runner("testsStarting").run(this);
1033
1281
  debug_default('running tests using "%s" file, options %O', this.scriptFile, this.options);
1034
1282
  return new Promise(async (resolve) => {
1035
- const scriptArgs = this.#convertOptionsToArgs().concat(this.options.scriptArgs).concat(
1036
- this.#convertFiltersToArgs({
1037
- ...this.options.filters,
1038
- ...filters
1039
- })
1040
- );
1283
+ const mergedFilters = { ...this.options.filters, ...filters };
1284
+ const scriptArgs = [
1285
+ ...this.#convertOptionsToArgs(),
1286
+ ...this.options.scriptArgs,
1287
+ ...this.#convertFiltersToArgs(mergedFilters)
1288
+ ];
1041
1289
  this.#testsProcess = runNode(this.cwd, {
1042
1290
  script: this.scriptFile,
1043
1291
  reject: true,
@@ -1066,41 +1314,73 @@ var TestRunner = class {
1066
1314
  });
1067
1315
  }
1068
1316
  /**
1069
- * Handles file change event
1317
+ * Handles file change event during watch mode
1318
+ *
1319
+ * Determines whether to run specific tests or all tests based on
1320
+ * the type of file that changed. Test files trigger selective runs,
1321
+ * while other files trigger full test suite runs.
1322
+ *
1323
+ * @param filePath - The path of the changed file
1324
+ * @param action - The type of change (add, update, delete)
1070
1325
  */
1071
- #handleFileChange(filePath, action) {
1072
- const file = this.#fileSystem.inspect(filePath);
1326
+ #handleFileChange(relativePath, absolutePath, action) {
1327
+ const file = this.#fileSystem.inspect(absolutePath, relativePath);
1073
1328
  if (!file) {
1074
1329
  return;
1075
1330
  }
1076
1331
  this.#clearScreen();
1077
- this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
1332
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
1078
1333
  if (file.fileType === "test") {
1079
- this.#reRunTests({ files: [filePath] });
1334
+ this.#reRunTests({ files: [relativePath] });
1080
1335
  } else {
1081
1336
  this.#reRunTests();
1082
1337
  }
1083
1338
  }
1084
1339
  /**
1085
- * Registers inline hooks for the file changes and restarts the
1086
- * HTTP server when a file gets changed.
1340
+ * Re-generates the index when a file is changed, but only in HMR
1341
+ * mode
1342
+ */
1343
+ #regenerateIndex(filePath, action) {
1344
+ if (action === "add") {
1345
+ return this.#indexGenerator.addFile(filePath);
1346
+ }
1347
+ return this.#indexGenerator.removeFile(filePath);
1348
+ }
1349
+ /**
1350
+ * Registers inline hooks for file changes and test re-runs
1351
+ *
1352
+ * Sets up event handlers that respond to file system changes by
1353
+ * triggering appropriate test runs based on the changed files.
1087
1354
  */
1088
1355
  #registerServerRestartHooks() {
1089
- this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
1090
- this.#hooks.add("fileChanged", (filePath) => this.#handleFileChange(filePath, "update"));
1091
- this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
1356
+ this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
1357
+ this.#regenerateIndex(absolutePath, "add");
1358
+ this.#handleFileChange(relativePath, absolutePath, "add");
1359
+ });
1360
+ this.#hooks.add("fileChanged", (relativePath, absolutePath) => {
1361
+ this.#regenerateIndex(absolutePath, "add");
1362
+ this.#handleFileChange(relativePath, absolutePath, "update");
1363
+ });
1364
+ this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
1365
+ this.#regenerateIndex(absolutePath, "delete");
1366
+ this.#handleFileChange(relativePath, absolutePath, "delete");
1367
+ });
1092
1368
  }
1093
1369
  /**
1094
- * Add listener to get notified when dev server is
1095
- * closed
1370
+ * Add listener to get notified when test runner is closed
1371
+ *
1372
+ * @param callback - Function to call when test runner closes
1373
+ * @returns This TestRunner instance for method chaining
1096
1374
  */
1097
1375
  onClose(callback) {
1098
1376
  this.#onClose = callback;
1099
1377
  return this;
1100
1378
  }
1101
1379
  /**
1102
- * Add listener to get notified when dev server exists
1103
- * with an error
1380
+ * Add listener to get notified when test runner encounters an error
1381
+ *
1382
+ * @param callback - Function to call when test runner encounters an error
1383
+ * @returns This TestRunner instance for method chaining
1104
1384
  */
1105
1385
  onError(callback) {
1106
1386
  this.#onError = callback;
@@ -1108,6 +1388,9 @@ var TestRunner = class {
1108
1388
  }
1109
1389
  /**
1110
1390
  * Close watchers and running child processes
1391
+ *
1392
+ * Cleans up file system watchers and terminates any running test
1393
+ * processes to ensure graceful shutdown.
1111
1394
  */
1112
1395
  async close() {
1113
1396
  await this.#watcher?.close();
@@ -1117,31 +1400,44 @@ var TestRunner = class {
1117
1400
  }
1118
1401
  }
1119
1402
  /**
1120
- * Runs tests
1403
+ * Runs tests once without watching for file changes
1404
+ *
1405
+ * Executes the test suite a single time and exits. This is the
1406
+ * equivalent of running tests in CI/CD environments.
1121
1407
  */
1122
1408
  async run() {
1123
1409
  this.#stickyPort = String(await getPort(this.cwd));
1124
- this.#hooks = await loadHooks(this.options.hooks, [
1125
- "testsStarting",
1126
- "testsFinished",
1127
- "fileAdded",
1128
- "fileChanged",
1129
- "fileRemoved"
1130
- ]);
1410
+ this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1131
1411
  this.#clearScreen();
1412
+ this.ui.logger.info("loading hooks...");
1413
+ this.#hooks = await loadHooks(this.options.hooks, ["init", "testsStarting", "testsFinished"]);
1414
+ await this.#hooks.runner("init").run(this, this.#indexGenerator);
1415
+ this.#hooks.clear("init");
1416
+ this.ui.logger.info("generating indexes...");
1417
+ await this.#indexGenerator.generate();
1132
1418
  this.ui.logger.info("booting application to run tests...");
1133
1419
  await this.#runTests(this.#stickyPort);
1134
1420
  }
1135
1421
  /**
1136
- * Run tests in watch mode
1422
+ * Run tests in watch mode and re-run them when files change
1423
+ *
1424
+ * Starts the test runner in watch mode, monitoring the file system
1425
+ * for changes and automatically re-running tests when relevant files
1426
+ * are modified. Uses intelligent filtering to run only affected tests
1427
+ * when possible.
1428
+ *
1429
+ * @param ts - TypeScript module reference for parsing configuration
1430
+ * @param options - Watch options including polling mode for file system monitoring
1137
1431
  */
1138
1432
  async runAndWatch(ts, options) {
1139
1433
  const tsConfig = parseConfig(this.cwd, ts);
1140
1434
  if (!tsConfig) {
1435
+ this.#onError?.(new RuntimeException2("Unable to parse tsconfig file"));
1141
1436
  return;
1142
1437
  }
1143
1438
  this.#stickyPort = String(await getPort(this.cwd));
1144
- this.#fileSystem = new FileSystem(this.cwd, tsConfig, {
1439
+ this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1440
+ this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, {
1145
1441
  ...this.options,
1146
1442
  suites: this.options.suites?.filter((suite) => {
1147
1443
  if (this.options.filters.suites) {
@@ -1150,7 +1446,10 @@ var TestRunner = class {
1150
1446
  return true;
1151
1447
  })
1152
1448
  });
1449
+ this.#clearScreen();
1450
+ this.ui.logger.info("loading hooks...");
1153
1451
  this.#hooks = await loadHooks(this.options.hooks, [
1452
+ "init",
1154
1453
  "testsStarting",
1155
1454
  "testsFinished",
1156
1455
  "fileAdded",
@@ -1158,12 +1457,15 @@ var TestRunner = class {
1158
1457
  "fileRemoved"
1159
1458
  ]);
1160
1459
  this.#registerServerRestartHooks();
1161
- this.#clearScreen();
1460
+ await this.#hooks.runner("init").run(this, this.#indexGenerator);
1461
+ this.#hooks.clear("init");
1462
+ this.ui.logger.info("generating indexes...");
1463
+ await this.#indexGenerator.generate();
1162
1464
  this.ui.logger.info("booting application to run tests...");
1163
1465
  await this.#runTests(this.#stickyPort);
1164
1466
  this.#watcher = watch({
1165
1467
  usePolling: options?.poll ?? false,
1166
- cwd: fileURLToPath5(this.cwd),
1468
+ cwd: this.cwdPath,
1167
1469
  ignoreInitial: true,
1168
1470
  ignored: (file, stats) => {
1169
1471
  if (!stats) {
@@ -1184,30 +1486,37 @@ var TestRunner = class {
1184
1486
  this.#onError?.(error);
1185
1487
  this.#watcher?.close();
1186
1488
  });
1187
- this.#watcher.on(
1188
- "add",
1189
- (filePath) => this.#hooks.runner("fileAdded").run(string4.toUnixSlash(filePath), this)
1190
- );
1191
- this.#watcher.on(
1192
- "change",
1193
- (filePath) => this.#hooks.runner("fileChanged").run(
1194
- string4.toUnixSlash(filePath),
1489
+ this.#watcher.on("add", (filePath) => {
1490
+ const relativePath = string4.toUnixSlash(filePath);
1491
+ const absolutePath = join3(this.cwdPath, filePath);
1492
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1493
+ });
1494
+ this.#watcher.on("change", (filePath) => {
1495
+ const relativePath = string4.toUnixSlash(filePath);
1496
+ const absolutePath = join3(this.cwdPath, filePath);
1497
+ this.#hooks.runner("fileChanged").run(
1498
+ relativePath,
1499
+ absolutePath,
1195
1500
  {
1196
1501
  source: "watcher",
1197
1502
  fullReload: true,
1198
1503
  hotReloaded: false
1199
1504
  },
1200
1505
  this
1201
- )
1202
- );
1203
- this.#watcher.on(
1204
- "unlink",
1205
- (filePath) => this.#hooks.runner("fileRemoved").run(string4.toUnixSlash(filePath), this)
1206
- );
1506
+ );
1507
+ });
1508
+ this.#watcher.on("unlink", (filePath) => {
1509
+ const relativePath = string4.toUnixSlash(filePath);
1510
+ const absolutePath = join3(this.cwdPath, filePath);
1511
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1512
+ });
1207
1513
  }
1208
1514
  };
1209
1515
  export {
1210
1516
  Bundler,
1211
1517
  DevServer,
1212
- TestRunner
1518
+ FileBuffer,
1519
+ SUPPORTED_PACKAGE_MANAGERS,
1520
+ TestRunner,
1521
+ VirtualFileSystem
1213
1522
  };