@adonisjs/assembler 8.0.0-next.8 → 8.0.0

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 +260 -0
  2. package/build/codemod_exception-CzQgXAAf.js +137 -0
  3. package/build/index.d.ts +3 -1
  4. package/build/index.js +927 -1720
  5. package/build/source-dVeugJ0e.js +166 -0
  6. package/build/src/bundler.d.ts +2 -0
  7. package/build/src/code_scanners/routes_scanner/main.d.ts +16 -2
  8. package/build/src/code_scanners/routes_scanner/main.js +168 -441
  9. package/build/src/code_transformer/main.d.ts +14 -1
  10. package/build/src/code_transformer/main.js +502 -622
  11. package/build/src/code_transformer/rc_file_transformer.d.ts +28 -2
  12. package/build/src/debug.d.ts +1 -1
  13. package/build/src/dev_server.d.ts +60 -12
  14. package/build/src/exceptions/codemod_exception.d.ts +178 -0
  15. package/build/src/file_buffer.d.ts +22 -2
  16. package/build/src/file_system.d.ts +2 -2
  17. package/build/src/helpers.js +72 -16
  18. package/build/src/index_generator/main.js +28 -6
  19. package/build/src/paths_resolver.d.ts +2 -1
  20. package/build/src/test_runner.d.ts +3 -2
  21. package/build/src/types/code_scanners.d.ts +29 -13
  22. package/build/src/types/code_transformer.d.ts +127 -0
  23. package/build/src/types/common.d.ts +98 -2
  24. package/build/src/types/hooks.d.ts +4 -1
  25. package/build/src/types/main.js +1 -0
  26. package/build/src/utils.d.ts +9 -3
  27. package/build/src/virtual_file_system.d.ts +1 -1
  28. package/build/validator_extractor-Ccio_Ndi.js +82 -0
  29. package/build/virtual_file_system-bGeoWsK-.js +285 -0
  30. package/package.json +41 -39
  31. package/build/chunk-N6H4XTTC.js +0 -405
  32. package/build/chunk-PORDZS62.js +0 -391
  33. package/build/chunk-TIKQQRMX.js +0 -116
  34. package/build/src/hooks.d.ts +0 -224
package/build/index.js CHANGED
@@ -1,374 +1,94 @@
1
- import {
2
- IndexGenerator
3
- } from "./chunk-N6H4XTTC.js";
4
- import {
5
- copyFiles,
6
- debug_default,
7
- getPort,
8
- loadHooks,
9
- memoize,
10
- parseConfig,
11
- run,
12
- runNode,
13
- throttle,
14
- watch
15
- } from "./chunk-PORDZS62.js";
16
-
17
- // src/hooks.ts
18
- var hooks = {
19
- /**
20
- * Hook called during application initialization. This is the first hook
21
- * that gets executed when the assembler starts up.
22
- *
23
- * @param callback - Function to execute when the init event occurs
24
- *
25
- * @example
26
- * ```js
27
- * hooks.init((app) => {
28
- * console.log('Application is initializing')
29
- * // Setup global configurations
30
- * })
31
- * ```
32
- */
33
- init(callback) {
34
- return callback;
35
- },
36
- /**
37
- * Hook called after routes have been committed to the router.
38
- * This occurs after all route definitions have been processed.
39
- *
40
- * @param callback - Function to execute when routes are committed
41
- *
42
- * @example
43
- * ```js
44
- * hooks.routesCommitted((router) => {
45
- * console.log('All routes have been committed to the router')
46
- * // Perform route-based setup
47
- * })
48
- * ```
49
- */
50
- routesCommitted(callback) {
51
- return callback;
52
- },
53
- /**
54
- * Hook called when the assembler starts scanning for route files.
55
- * This happens before any route files are actually processed.
56
- *
57
- * @param callback - Function to execute when route scanning begins
58
- *
59
- * @example
60
- * ```js
61
- * hooks.routesScanning(() => {
62
- * console.log('Starting to scan for route files')
63
- * // Setup route scanning configurations
64
- * })
65
- * ```
66
- */
67
- routesScanning(callback) {
68
- return callback;
69
- },
70
- /**
71
- * Hook called after all route files have been scanned and processed.
72
- * This occurs once the route scanning phase is complete.
73
- *
74
- * @param callback - Function to execute when route scanning is finished
75
- *
76
- * @example
77
- * ```js
78
- * hooks.routesScanned((scannedRoutes) => {
79
- * console.log('Route scanning completed')
80
- * // Process scanned route information
81
- * })
82
- * ```
83
- */
84
- routesScanned(callback) {
85
- return callback;
86
- },
87
- /**
88
- * Hook called when a file is modified during development.
89
- * This is triggered by the file watcher when changes are detected.
90
- *
91
- * @param callback - Function to execute when a file changes
92
- *
93
- * @example
94
- * ```js
95
- * hooks.fileChanged((filePath, stats) => {
96
- * console.log(`File changed: ${filePath}`)
97
- * // Handle file change logic
98
- * })
99
- * ```
100
- */
101
- fileChanged(callback) {
102
- return callback;
103
- },
104
- /**
105
- * Hook called when a new file is added during development.
106
- * This is triggered by the file watcher when new files are created.
107
- *
108
- * @param callback - Function to execute when a file is added
109
- *
110
- * @example
111
- * ```js
112
- * hooks.fileAdded((filePath, stats) => {
113
- * console.log(`New file added: ${filePath}`)
114
- * // Handle new file logic
115
- * })
116
- * ```
117
- */
118
- fileAdded(callback) {
119
- return callback;
120
- },
121
- /**
122
- * Hook called when a file is removed during development.
123
- * This is triggered by the file watcher when files are deleted.
124
- *
125
- * @param callback - Function to execute when a file is removed
126
- *
127
- * @example
128
- * ```js
129
- * hooks.fileRemoved((filePath) => {
130
- * console.log(`File removed: ${filePath}`)
131
- * // Handle file removal logic
132
- * })
133
- * ```
134
- */
135
- fileRemoved(callback) {
136
- return callback;
137
- },
138
- /**
139
- * Hook called when the development server is about to start.
140
- * This occurs before the server begins listening for connections.
141
- *
142
- * @param callback - Function to execute when dev server is starting
143
- *
144
- * @example
145
- * ```js
146
- * hooks.devServerStarting((server) => {
147
- * console.log('Development server is starting')
148
- * // Setup server configurations
149
- * })
150
- * ```
151
- */
152
- devServerStarting(callback) {
153
- return callback;
154
- },
155
- /**
156
- * Hook called after the development server has successfully started.
157
- * This occurs once the server is listening and ready to accept requests.
158
- *
159
- * @param callback - Function to execute when dev server has started
160
- *
161
- * @example
162
- * ```js
163
- * hooks.devServerStarted((server) => {
164
- * console.log(`Development server started on port ${server.port}`)
165
- * // Notify external services or open browser
166
- * })
167
- * ```
168
- */
169
- devServerStarted(callback) {
170
- return callback;
171
- },
172
- /**
173
- * Hook called when the build process is about to start.
174
- * This occurs before any build tasks are executed.
175
- *
176
- * @param callback - Function to execute when build is starting
177
- *
178
- * @example
179
- * ```js
180
- * hooks.buildStarting((buildConfig) => {
181
- * console.log('Build process is starting')
182
- * // Setup build configurations or clean directories
183
- * })
184
- * ```
185
- */
186
- buildStarting(callback) {
187
- return callback;
188
- },
189
- /**
190
- * Hook called after the build process has completed.
191
- * This occurs once all build tasks have finished executing.
192
- *
193
- * @param callback - Function to execute when build is finished
194
- *
195
- * @example
196
- * ```js
197
- * hooks.buildFinished((buildResult) => {
198
- * console.log('Build process completed')
199
- * // Deploy artifacts or notify build completion
200
- * })
201
- * ```
202
- */
203
- buildFinished(callback) {
204
- return callback;
205
- },
206
- /**
207
- * Hook called when the test suite is about to start.
208
- * This occurs before any test files are executed.
209
- *
210
- * @param callback - Function to execute when tests are starting
211
- *
212
- * @example
213
- * ```js
214
- * hooks.testsStarting((testConfig) => {
215
- * console.log('Test suite is starting')
216
- * // Setup test database or mock services
217
- * })
218
- * ```
219
- */
220
- testsStarting(callback) {
221
- return callback;
222
- },
223
- /**
224
- * Hook called after the test suite has completed.
225
- * This occurs once all test files have finished executing.
226
- *
227
- * @param callback - Function to execute when tests are finished
228
- *
229
- * @example
230
- * ```js
231
- * hooks.testsFinished((testResults) => {
232
- * console.log('Test suite completed')
233
- * // Generate test reports or cleanup test resources
234
- * })
235
- * ```
236
- */
237
- testsFinished(callback) {
238
- return callback;
239
- }
240
- };
241
-
242
- // src/bundler.ts
1
+ import { a as loadHooks, c as readTsConfig, d as runNode, f as throttle, m as debug_default, n as copyFiles, o as memoize, p as watch, r as getPort, s as parseConfig, t as VirtualFileSystem, u as run } from "./virtual_file_system-bGeoWsK-.js";
2
+ import { n as FileBuffer } from "./source-dVeugJ0e.js";
3
+ import { IndexGenerator } from "./src/index_generator/main.js";
4
+ import "./validator_extractor-Ccio_Ndi.js";
5
+ import { RoutesScanner } from "./src/code_scanners/routes_scanner/main.js";
6
+ import { t as CodemodException } from "./codemod_exception-CzQgXAAf.js";
243
7
  import dedent from "dedent";
244
- import fs from "fs/promises";
8
+ import fs, { readFile, unlink } from "node:fs/promises";
245
9
  import { cliui } from "@poppinss/cliui";
246
- import { fileURLToPath } from "url";
10
+ import { fileURLToPath } from "node:url";
247
11
  import string from "@poppinss/utils/string";
248
- import { join, relative } from "path/posix";
12
+ import { join, relative } from "node:path/posix";
249
13
  import { detectPackageManager } from "@antfu/install-pkg";
250
- var SUPPORTED_PACKAGE_MANAGERS = {
251
- "npm": {
252
- packageManagerFiles: ["package-lock.json"],
253
- installCommand: 'npm ci --omit="dev"'
254
- },
255
- "yarn": {
256
- packageManagerFiles: ["yarn.lock"],
257
- installCommand: "yarn install --production"
258
- },
259
- "yarn@berry": {
260
- packageManagerFiles: ["yarn.lock", ".yarn/**/*", ".yarnrc.yml"],
261
- installCommand: "yarn workspaces focus --production"
262
- },
263
- "pnpm": {
264
- packageManagerFiles: ["pnpm-lock.yaml"],
265
- installCommand: "pnpm i --prod"
266
- },
267
- "bun": {
268
- packageManagerFiles: ["bun.lockb"],
269
- installCommand: "bun install --production"
270
- }
14
+ import getRandomPort from "get-port";
15
+ import picomatch from "picomatch";
16
+ import prettyHrtime from "pretty-hrtime";
17
+ import { RuntimeException } from "@poppinss/utils/exception";
18
+ const SUPPORTED_PACKAGE_MANAGERS = {
19
+ "npm": {
20
+ packageManagerFiles: ["package-lock.json"],
21
+ installCommand: "npm ci --omit=\"dev\""
22
+ },
23
+ "yarn": {
24
+ packageManagerFiles: ["yarn.lock"],
25
+ installCommand: "yarn install --production"
26
+ },
27
+ "yarn@berry": {
28
+ packageManagerFiles: [
29
+ "yarn.lock",
30
+ ".yarn/**/*",
31
+ ".yarnrc.yml"
32
+ ],
33
+ installCommand: "yarn workspaces focus --production"
34
+ },
35
+ "pnpm": {
36
+ packageManagerFiles: ["pnpm-lock.yaml"],
37
+ installCommand: "pnpm i --prod"
38
+ },
39
+ "bun": {
40
+ packageManagerFiles: ["bun.lockb"],
41
+ installCommand: "bun install --production"
42
+ }
271
43
  };
272
44
  var Bundler = class {
273
- /**
274
- * Reference to the TypeScript module
275
- */
276
- #ts;
277
- /**
278
- * Hooks to execute custom actions during the build process
279
- */
280
- #hooks;
281
- /**
282
- * Index generator for managing auto-generated index files
283
- */
284
- #indexGenerator;
285
- /**
286
- * CLI UI instance for displaying colorful messages and progress information
287
- */
288
- ui = cliui();
289
- /**
290
- * The current working directory URL
291
- */
292
- cwd;
293
- /**
294
- * The current working project directory path as string
295
- */
296
- cwdPath;
297
- /**
298
- * Bundler configuration options including hooks and meta files
299
- */
300
- options;
301
- /**
302
- * Create a new bundler instance
303
- *
304
- * @param cwd - The current working directory URL
305
- * @param ts - TypeScript module reference
306
- * @param options - Bundler configuration options
307
- */
308
- constructor(cwd, ts, options) {
309
- this.cwd = cwd;
310
- this.options = options;
311
- this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
312
- this.#ts = ts;
313
- }
314
- /**
315
- * Returns the relative unix path for an absolute
316
- * file path
317
- */
318
- #getRelativeName(filePath) {
319
- return string.toUnixSlash(relative(this.cwdPath, filePath));
320
- }
321
- /**
322
- * Cleans up the build directory
323
- */
324
- async #cleanupBuildDirectory(outDir) {
325
- await fs.rm(outDir, { recursive: true, force: true, maxRetries: 5 });
326
- }
327
- /**
328
- * Runs tsc command to build the source.
329
- */
330
- async #runTsc(outDir) {
331
- try {
332
- await run(this.cwd, {
333
- stdio: "inherit",
334
- script: "tsc",
335
- scriptArgs: ["--outDir", outDir]
336
- });
337
- return true;
338
- } catch {
339
- return false;
340
- }
341
- }
342
- /**
343
- * Copy meta files to the output directory
344
- */
345
- async #copyMetaFiles(outDir, additionalFilesToCopy) {
346
- const metaFiles = (this.options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy);
347
- await copyFiles(metaFiles, this.cwdPath, outDir);
348
- }
349
- /**
350
- * Detect the package manager used by the project
351
- */
352
- async #detectPackageManager() {
353
- const pkgManager = await detectPackageManager(this.cwdPath);
354
- if (pkgManager === "deno") {
355
- return "npm";
356
- }
357
- if (pkgManager === "pnpm@6") {
358
- return "pnpm";
359
- }
360
- return pkgManager;
361
- }
362
- /**
363
- * Rewrite the ace file since the original one
364
- * is importing ts-node which is not installed
365
- * in a production environment.
366
- */
367
- async #createAceFile(outDir) {
368
- const aceFileLocation = join(outDir, "ace.js");
369
- const aceFileContent = dedent(
370
- /* JavaScript */
371
- `
45
+ #ts;
46
+ #hooks;
47
+ #indexGenerator;
48
+ ui = cliui();
49
+ cwd;
50
+ cwdPath;
51
+ options;
52
+ constructor(cwd, ts, options) {
53
+ this.cwd = cwd;
54
+ this.options = options;
55
+ this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
56
+ this.#ts = ts;
57
+ }
58
+ #getRelativeName(filePath) {
59
+ return string.toUnixSlash(relative(this.cwdPath, filePath));
60
+ }
61
+ async #cleanupBuildDirectory(outDir) {
62
+ await fs.rm(outDir, {
63
+ recursive: true,
64
+ force: true,
65
+ maxRetries: 5
66
+ });
67
+ }
68
+ async #runTsc(outDir) {
69
+ try {
70
+ await run(this.cwd, {
71
+ stdio: "inherit",
72
+ script: "tsc",
73
+ scriptArgs: ["--outDir", outDir]
74
+ });
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ async #copyMetaFiles(outDir, additionalFilesToCopy) {
81
+ await copyFiles((this.options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy), this.cwdPath, outDir);
82
+ }
83
+ async #detectPackageManager() {
84
+ const pkgManager = await detectPackageManager(this.cwdPath);
85
+ if (pkgManager === "deno") return "npm";
86
+ if (pkgManager === "pnpm@6") return "pnpm";
87
+ return pkgManager;
88
+ }
89
+ async #createAceFile(outDir) {
90
+ const aceFileLocation = join(outDir, "ace.js");
91
+ const aceFileContent = dedent(`
372
92
  /**
373
93
  * This file is auto-generated by the build process.
374
94
  * If you had any custom code inside this file, then
@@ -376,1366 +96,853 @@ var Bundler = class {
376
96
  */
377
97
 
378
98
  await import('./bin/console.js')
379
- `
380
- );
381
- await fs.writeFile(aceFileLocation, aceFileContent);
382
- this.ui.logger.info("created ace file", { suffix: this.#getRelativeName(aceFileLocation) });
383
- }
384
- /**
385
- * Bundles the application to be run in production
386
- *
387
- * @param stopOnError - Whether to stop the build process on TypeScript errors
388
- * @param client - Override the detected package manager
389
- * @returns Promise that resolves to true if build succeeded, false otherwise
390
- *
391
- * @example
392
- * const success = await bundler.bundle(true, 'npm')
393
- */
394
- async bundle(stopOnError = true, client) {
395
- this.packageManager = client ?? await this.#detectPackageManager() ?? "npm";
396
- const config = parseConfig(this.cwd, this.#ts);
397
- if (!config) {
398
- return false;
399
- }
400
- this.ui.logger.info("loading hooks...");
401
- this.#hooks = await loadHooks(this.options.hooks, ["init", "buildStarting", "buildFinished"]);
402
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
403
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
404
- this.#hooks.clear("init");
405
- this.ui.logger.info("generating indexes...");
406
- await this.#indexGenerator.generate();
407
- const outDir = config.options.outDir || fileURLToPath(new URL("build/", this.cwd));
408
- this.ui.logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
409
- await this.#cleanupBuildDirectory(outDir);
410
- await this.#hooks.runner("buildStarting").run(this);
411
- this.ui.logger.info("compiling typescript source", { suffix: "tsc" });
412
- const buildCompleted = await this.#runTsc(outDir);
413
- await this.#createAceFile(outDir);
414
- if (!buildCompleted && stopOnError) {
415
- await this.#cleanupBuildDirectory(outDir);
416
- const instructions = this.ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
417
- instructions.add(
418
- this.ui.colors.red("Cannot complete the build process as there are TypeScript errors.")
419
- );
420
- instructions.add(
421
- this.ui.colors.red(
422
- 'Use "--ignore-ts-errors" flag to ignore TypeScript errors and continue the build.'
423
- )
424
- );
425
- this.ui.logger.logError(instructions.prepare());
426
- return false;
427
- }
428
- const pkgFiles = [
429
- "package.json",
430
- ...SUPPORTED_PACKAGE_MANAGERS[this.packageManager].packageManagerFiles
431
- ];
432
- this.ui.logger.info("copying meta files to the output directory");
433
- await this.#copyMetaFiles(outDir, pkgFiles);
434
- this.ui.logger.success("build completed");
435
- this.ui.logger.log("");
436
- const displayMessage = this.ui.instructions().heading("Run the following commands to start the server in production");
437
- await this.#hooks.runner("buildFinished").run(this, displayMessage);
438
- displayMessage.add(this.ui.colors.cyan(`cd ${this.#getRelativeName(outDir)}`)).add(this.ui.colors.cyan(SUPPORTED_PACKAGE_MANAGERS[this.packageManager].installCommand)).add(this.ui.colors.cyan("node bin/server.js")).render();
439
- return true;
440
- }
99
+ `);
100
+ await fs.writeFile(aceFileLocation, aceFileContent);
101
+ this.ui.logger.info("created ace file", { suffix: this.#getRelativeName(aceFileLocation) });
102
+ }
103
+ async bundle(stopOnError = true, client) {
104
+ this.packageManager = client ?? await this.#detectPackageManager() ?? "npm";
105
+ const config = parseConfig(this.cwd, this.#ts);
106
+ if (!config) return false;
107
+ this.ui.logger.info("loading hooks...");
108
+ this.#hooks = await loadHooks(this.options.hooks, [
109
+ "init",
110
+ "buildStarting",
111
+ "buildFinished"
112
+ ]);
113
+ this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
114
+ await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
115
+ this.#hooks.clear("init");
116
+ this.ui.logger.info("generating indexes...");
117
+ await this.#indexGenerator.generate();
118
+ const outDir = config.options.outDir || fileURLToPath(new URL("build/", this.cwd));
119
+ this.ui.logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
120
+ await this.#cleanupBuildDirectory(outDir);
121
+ await this.#hooks.runner("buildStarting").run(this);
122
+ this.ui.logger.info("compiling typescript source", { suffix: "tsc" });
123
+ const buildCompleted = await this.#runTsc(outDir);
124
+ await this.#createAceFile(outDir);
125
+ if (!buildCompleted && stopOnError) {
126
+ await this.#cleanupBuildDirectory(outDir);
127
+ const instructions = this.ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
128
+ instructions.add(this.ui.colors.red("Cannot complete the build process as there are TypeScript errors."));
129
+ instructions.add(this.ui.colors.red("Use \"--ignore-ts-errors\" flag to ignore TypeScript errors and continue the build."));
130
+ this.ui.logger.logError(instructions.prepare());
131
+ return false;
132
+ }
133
+ const pkgFiles = ["package.json", ...SUPPORTED_PACKAGE_MANAGERS[this.packageManager].packageManagerFiles];
134
+ this.ui.logger.info("copying meta files to the output directory");
135
+ await this.#copyMetaFiles(outDir, pkgFiles);
136
+ this.ui.logger.success("build completed");
137
+ this.ui.logger.log("");
138
+ const displayMessage = this.ui.instructions().heading("Run the following commands to start the server in production");
139
+ await this.#hooks.runner("buildFinished").run(this, displayMessage);
140
+ displayMessage.add(this.ui.colors.cyan(`cd ${this.#getRelativeName(outDir)}`)).add(this.ui.colors.cyan(SUPPORTED_PACKAGE_MANAGERS[this.packageManager].installCommand)).add(this.ui.colors.cyan("node bin/server.js")).render();
141
+ return true;
142
+ }
441
143
  };
442
-
443
- // src/dev_server.ts
444
- import { cliui as cliui2 } from "@poppinss/cliui";
445
- import prettyHrtime from "pretty-hrtime";
446
- import { fileURLToPath as fileURLToPath2 } from "url";
447
- import string3 from "@poppinss/utils/string";
448
- import { join as join2, relative as relative3 } from "path/posix";
449
- import { RuntimeException } from "@poppinss/utils/exception";
450
-
451
- // src/file_system.ts
452
- import picomatch from "picomatch";
453
- import { relative as relative2 } from "path/posix";
454
- import string2 from "@poppinss/utils/string";
455
- var DEFAULT_INCLUDES = ["**/*"];
456
- var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**", ".adonisjs/**"];
457
- var DEFAULT_EXCLUDES = ["node_modules/**", "bower_components/**", "jspm_packages/**"];
144
+ const DEFAULT_INCLUDES = ["**/*"];
145
+ const ALWAYS_EXCLUDE = [
146
+ ".git/**",
147
+ "coverage/**",
148
+ ".github/**",
149
+ ".adonisjs/**"
150
+ ];
151
+ const DEFAULT_EXCLUDES = [
152
+ "node_modules/**",
153
+ "bower_components/**",
154
+ "jspm_packages/**"
155
+ ];
458
156
  var FileSystem = class {
459
- /**
460
- * The current working project directory
461
- */
462
- #cwd;
463
- /**
464
- * Referenced to the parsed ts config file. We use it to read the includes,
465
- * excludes and pre-scanned files.
466
- */
467
- #tsConfig;
468
- /**
469
- * Set of pre-scanned typeScript files provided by tsconfig
470
- */
471
- #scannedTypeScriptFiles = /* @__PURE__ */ new Set();
472
- /**
473
- * Picomatch matcher function to know if a file path is
474
- * part of includes
475
- */
476
- #isIncluded;
477
- /**
478
- * Picomatch matcher function to know if a file path is
479
- * part of excludes
480
- */
481
- #isExcluded;
482
- /**
483
- * Picomatch matcher function to know if a file path is a
484
- * meta file with reloadServer option enabled
485
- */
486
- #isMetaFileWithReloadsEnabled;
487
- /**
488
- * Picomatch matcher function to know if a file path is a
489
- * meta file with reloadServer option disabled
490
- */
491
- #isMetaFileWithReloadsDisabled;
492
- /**
493
- * Picomatch matcher function to know if a file path is a
494
- * test file or not
495
- */
496
- #isTestFile;
497
- /**
498
- * References to includes and excludes glob patterns
499
- */
500
- #includes;
501
- #excludes;
502
- /**
503
- * Includes glob patterns extracted from "tsconfig.json" file.
504
- * Defaults to: ["**\/*"]
505
- */
506
- get includes() {
507
- return this.#includes;
508
- }
509
- /**
510
- * Excludes glob patterns extracted from "tsconfig.json" file.
511
- *
512
- * Defaults to: [
513
- * 'node_modules/**',
514
- * 'bower_components/**',
515
- * 'jspm_packages/**,
516
- * ]
517
- *
518
- * Following patterns are always ignored
519
- *
520
- * '.git/**', 'coverage/**', '.github/**'
521
- */
522
- get excludes() {
523
- return this.#excludes;
524
- }
525
- /**
526
- * Inspect a file path to determine its type and properties within the project.
527
- *
528
- * This method analyzes a file to categorize it as a script file, test file, or meta file,
529
- * and determines whether changes to the file should trigger server restarts. Results
530
- * are memoized for performance optimization.
531
- *
532
- * @param absolutePath - The absolute Unix path to the file
533
- * @param relativePath - The relative Unix path from the project root
534
- * @returns File inspection result or null if the file should be ignored
535
- *
536
- * @example
537
- * const file = fileSystem.inspect('/project/app/models/user.ts', 'app/models/user.ts')
538
- * if (file) {
539
- * console.log(file.fileType) // 'script'
540
- * console.log(file.reloadServer) // true
541
- * }
542
- */
543
- inspect = memoize((absolutePath, relativePath) => {
544
- relativePath = relativePath ?? relative2(this.#cwd, absolutePath);
545
- if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
546
- debug_default('backend project file "%s"', relativePath);
547
- const isTestFile = this.#isTestFile(relativePath);
548
- return {
549
- fileType: isTestFile ? "test" : "script",
550
- reloadServer: !isTestFile,
551
- unixRelativePath: relativePath,
552
- unixAbsolutePath: absolutePath
553
- };
554
- }
555
- if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
556
- debug_default('meta file "%s"', relativePath);
557
- return {
558
- fileType: "meta",
559
- reloadServer: true,
560
- unixRelativePath: relativePath,
561
- unixAbsolutePath: absolutePath
562
- };
563
- }
564
- if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
565
- debug_default('meta file "%s"', relativePath);
566
- return {
567
- fileType: "meta",
568
- reloadServer: false,
569
- unixRelativePath: relativePath,
570
- unixAbsolutePath: absolutePath
571
- };
572
- }
573
- debug_default('ignored file "%s"', relativePath);
574
- return null;
575
- });
576
- /**
577
- * Determines if a directory should be watched by the file watcher.
578
- *
579
- * This method checks if a directory should be monitored for file changes
580
- * based on the TypeScript configuration includes/excludes patterns.
581
- * Results are memoized for performance. Chokidar sends absolute Unix paths.
582
- *
583
- * Note: Use shouldWatchFile for files and this method for directories only.
584
- *
585
- * @param absolutePath - The absolute Unix path to the directory
586
- * @returns True if the directory should be watched
587
- *
588
- * @example
589
- * const shouldWatch = fileSystem.shouldWatchDirectory('/project/app/controllers')
590
- * console.log(shouldWatch) // true
591
- */
592
- shouldWatchDirectory = memoize((absolutePath) => {
593
- if (absolutePath === this.#cwd) {
594
- debug_default("watching project root");
595
- return true;
596
- }
597
- const relativePath = relative2(this.#cwd, absolutePath);
598
- if (this.#isExcluded(relativePath)) {
599
- debug_default('watching "%s"', absolutePath);
600
- return false;
601
- }
602
- return true;
603
- });
604
- /**
605
- * Create a new FileSystem instance
606
- *
607
- * @param cwd - The current working directory URL or string path
608
- * @param tsConfig - Parsed TypeScript configuration
609
- * @param rcFile - AdonisJS RC file configuration
610
- */
611
- constructor(cwd, tsConfig, rcFile) {
612
- this.#cwd = cwd;
613
- this.#tsConfig = tsConfig;
614
- const files = tsConfig.fileNames;
615
- const metaFiles = rcFile.metaFiles ?? [];
616
- const testSuites = rcFile.suites ?? [];
617
- const outDir = tsConfig.raw.compilerOptions?.outDir;
618
- for (const file of files) {
619
- this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file));
620
- }
621
- this.#includes = tsConfig.raw.include || DEFAULT_INCLUDES;
622
- this.#excludes = ALWAYS_EXCLUDE.concat(
623
- tsConfig.raw.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES)
624
- );
625
- const metaFilesWithReloads = [];
626
- const metaFilesWithoutReloads = [];
627
- for (const file of metaFiles) {
628
- if (file.reloadServer) {
629
- metaFilesWithReloads.push(file.pattern);
630
- } else {
631
- metaFilesWithoutReloads.push(file.pattern);
632
- }
633
- }
634
- const testFilePatterns = testSuites.flatMap((suite) => suite.files);
635
- const picomatcchOptions = { cwd: this.#cwd };
636
- this.#isMetaFileWithReloadsEnabled = picomatch(metaFilesWithReloads, picomatcchOptions);
637
- this.#isMetaFileWithReloadsDisabled = picomatch(metaFilesWithoutReloads, picomatcchOptions);
638
- this.#isTestFile = picomatch(testFilePatterns, picomatcchOptions);
639
- this.#isIncluded = picomatch(this.#includes, picomatcchOptions);
640
- this.#isExcluded = picomatch(this.#excludes, picomatcchOptions);
641
- debug_default("initiating file system %O", {
642
- includes: this.#includes,
643
- excludes: this.#excludes,
644
- outDir,
645
- files,
646
- metaFiles,
647
- testSuites
648
- });
649
- }
650
- /**
651
- * Determines if a file path represents a script file based on TypeScript configuration.
652
- *
653
- * Script files are those that can be processed by the TypeScript compiler:
654
- * - Files ending with ".ts" or ".tsx" (excluding ".d.ts" declaration files)
655
- * - Files ending with ".js" when "allowJs" option is enabled in tsconfig
656
- * - Files ending with ".json" when "resolveJsonModule" option is enabled in tsconfig
657
- *
658
- * @param relativePath - The relative file path to check
659
- * @returns True if the file is a script file
660
- */
661
- #isScriptFile(relativePath) {
662
- if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
663
- return true;
664
- }
665
- if (this.#tsConfig.options.allowJs && relativePath.endsWith(".js")) {
666
- return true;
667
- }
668
- if (this.#tsConfig.options.resolveJsonModule && relativePath.endsWith(".json")) {
669
- return true;
670
- }
671
- return false;
672
- }
673
- /**
674
- * Checks if a file path is part of the backend TypeScript project.
675
- *
676
- * Uses TypeScript configuration "includes", "excludes", and "files" paths
677
- * to determine if a file should be considered part of the project compilation.
678
- *
679
- * @param relativePath - The relative file path to check
680
- * @returns True if the file is part of the backend project
681
- */
682
- #isPartOfBackendProject(relativePath) {
683
- if (this.#isExcluded(relativePath)) {
684
- debug_default('excluded by tsconfig "%s"', relativePath);
685
- return false;
686
- }
687
- if (this.#isIncluded(relativePath)) {
688
- debug_default('included by tsconfig "%s"', relativePath);
689
- return true;
690
- }
691
- return false;
692
- }
693
- /**
694
- * Returns true if the file should be watched. Chokidar sends
695
- * absolute unix paths to the ignored callback.
696
- *
697
- * You must use "shouldWatchDirectory" method for directories and call
698
- * this method for files only.
699
- *
700
- * @param absolutePath - The absolute path to the file
701
- * @returns True if the file should be watched
702
- */
703
- shouldWatchFile(absolutePath) {
704
- return this.inspect(absolutePath) !== null;
705
- }
157
+ #cwd;
158
+ #tsConfig;
159
+ #scannedTypeScriptFiles = /* @__PURE__ */ new Set();
160
+ #isIncluded;
161
+ #isExcluded;
162
+ #isMetaFileWithReloadsEnabled;
163
+ #isMetaFileWithReloadsDisabled;
164
+ #isTestFile;
165
+ #includes;
166
+ #excludes;
167
+ get includes() {
168
+ return this.#includes;
169
+ }
170
+ get excludes() {
171
+ return this.#excludes;
172
+ }
173
+ inspect = memoize((absolutePath, relativePath) => {
174
+ relativePath = relativePath ?? relative(this.#cwd, absolutePath);
175
+ if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
176
+ debug_default("backend project file \"%s\"", relativePath);
177
+ const isTestFile = this.#isTestFile(relativePath);
178
+ return {
179
+ fileType: isTestFile ? "test" : "script",
180
+ reloadServer: !isTestFile,
181
+ unixRelativePath: relativePath,
182
+ unixAbsolutePath: absolutePath
183
+ };
184
+ }
185
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
186
+ debug_default("meta file \"%s\"", relativePath);
187
+ return {
188
+ fileType: "meta",
189
+ reloadServer: true,
190
+ unixRelativePath: relativePath,
191
+ unixAbsolutePath: absolutePath
192
+ };
193
+ }
194
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
195
+ debug_default("meta file \"%s\"", relativePath);
196
+ return {
197
+ fileType: "meta",
198
+ reloadServer: false,
199
+ unixRelativePath: relativePath,
200
+ unixAbsolutePath: absolutePath
201
+ };
202
+ }
203
+ debug_default("ignored file \"%s\"", relativePath);
204
+ return null;
205
+ });
206
+ shouldWatchDirectory = memoize((absolutePath) => {
207
+ if (absolutePath === this.#cwd) {
208
+ debug_default("watching project root");
209
+ return true;
210
+ }
211
+ const relativePath = relative(this.#cwd, absolutePath);
212
+ if (this.#isExcluded(relativePath)) {
213
+ debug_default("watching \"%s\"", absolutePath);
214
+ return false;
215
+ }
216
+ return true;
217
+ });
218
+ constructor(cwd, tsConfig, rcFile) {
219
+ this.#cwd = cwd;
220
+ this.#tsConfig = tsConfig;
221
+ const files = tsConfig.config.files ?? [];
222
+ const metaFiles = rcFile.metaFiles ?? [];
223
+ const testSuites = rcFile.suites ?? [];
224
+ const outDir = tsConfig.config.compilerOptions?.outDir;
225
+ for (const file of files) this.#scannedTypeScriptFiles.add(string.toUnixSlash(file));
226
+ this.#includes = tsConfig.config.include || DEFAULT_INCLUDES;
227
+ this.#excludes = ALWAYS_EXCLUDE.concat(tsConfig.config.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES));
228
+ const metaFilesWithReloads = [];
229
+ const metaFilesWithoutReloads = [];
230
+ for (const file of metaFiles) if (file.reloadServer) metaFilesWithReloads.push(file.pattern);
231
+ else metaFilesWithoutReloads.push(file.pattern);
232
+ const testFilePatterns = testSuites.flatMap((suite) => suite.files);
233
+ const picomatcchOptions = { cwd: this.#cwd };
234
+ this.#isMetaFileWithReloadsEnabled = picomatch(metaFilesWithReloads, picomatcchOptions);
235
+ this.#isMetaFileWithReloadsDisabled = picomatch(metaFilesWithoutReloads, picomatcchOptions);
236
+ this.#isTestFile = picomatch(testFilePatterns, picomatcchOptions);
237
+ this.#isIncluded = picomatch(this.#includes, picomatcchOptions);
238
+ this.#isExcluded = picomatch(this.#excludes, picomatcchOptions);
239
+ debug_default("initiating file system %O", {
240
+ includes: this.#includes,
241
+ excludes: this.#excludes,
242
+ outDir,
243
+ files,
244
+ metaFiles,
245
+ testSuites
246
+ });
247
+ }
248
+ #isScriptFile(relativePath) {
249
+ if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) return true;
250
+ if (this.#tsConfig.config.compilerOptions?.allowJs && relativePath.endsWith(".js")) return true;
251
+ if (this.#tsConfig.config.compilerOptions?.resolveJsonModule && relativePath.endsWith(".json")) return true;
252
+ return false;
253
+ }
254
+ #isPartOfBackendProject(relativePath) {
255
+ if (this.#isExcluded(relativePath)) {
256
+ debug_default("excluded by tsconfig \"%s\"", relativePath);
257
+ return false;
258
+ }
259
+ if (this.#isIncluded(relativePath)) {
260
+ debug_default("included by tsconfig \"%s\"", relativePath);
261
+ return true;
262
+ }
263
+ return false;
264
+ }
265
+ shouldWatchFile(absolutePath) {
266
+ return this.inspect(absolutePath) !== null;
267
+ }
706
268
  };
707
-
708
- // src/shortcuts_manager.ts
709
269
  var ShortcutsManager = class {
710
- /**
711
- * Logger instance for displaying messages
712
- */
713
- #logger;
714
- /**
715
- * Callback functions for different keyboard shortcuts
716
- */
717
- #callbacks;
718
- /**
719
- * The server URL used for opening browser
720
- */
721
- #serverUrl;
722
- /**
723
- * Key press event handler function
724
- */
725
- #keyPressHandler;
726
- /**
727
- * Available keyboard shortcuts with their handlers
728
- */
729
- #shortcuts = [
730
- {
731
- key: "r",
732
- description: "restart server",
733
- handler: () => {
734
- this.#logger.log("");
735
- this.#logger.info("Manual restart triggered...");
736
- this.#callbacks.onRestart();
737
- }
738
- },
739
- {
740
- key: "c",
741
- description: "clear console",
742
- handler: () => {
743
- this.#callbacks.onClear();
744
- this.#logger.info("Console cleared");
745
- }
746
- },
747
- {
748
- key: "o",
749
- description: "open in browser",
750
- handler: () => this.#handleOpenBrowser()
751
- },
752
- {
753
- key: "h",
754
- description: "show this help",
755
- handler: () => this.showHelp()
756
- }
757
- ];
758
- /**
759
- * Create a new ShortcutsManager instance
760
- *
761
- * @param options - Configuration options for the shortcuts manager
762
- */
763
- constructor(options) {
764
- this.#logger = options.logger;
765
- this.#callbacks = options.callbacks;
766
- }
767
- /**
768
- * Set server url for opening in browser
769
- *
770
- * This URL will be used when the user presses 'o' to open the
771
- * development server in their default browser.
772
- *
773
- * @param url - The server URL to open when 'o' key is pressed
774
- */
775
- setServerUrl(url) {
776
- this.#serverUrl = url;
777
- }
778
- /**
779
- * Initialize keyboard shortcuts by setting up raw mode on stdin
780
- *
781
- * This method enables raw mode on stdin to capture individual keypresses
782
- * and sets up the event listener for handling keyboard input. Only works
783
- * in TTY environments.
784
- */
785
- setup() {
786
- if (!process.stdin.isTTY) {
787
- return;
788
- }
789
- process.stdin.setRawMode(true);
790
- this.#keyPressHandler = (data) => this.#handleKeyPress(data.toString());
791
- process.stdin.on("data", this.#keyPressHandler);
792
- }
793
- /**
794
- * Handle key press events and execute corresponding shortcuts
795
- *
796
- * Processes individual key presses and matches them against registered
797
- * shortcuts. Also handles special key combinations like Ctrl+C and Ctrl+D.
798
- *
799
- * @param key - The pressed key as a string
800
- */
801
- #handleKeyPress(key) {
802
- if (key === "" || key === "") {
803
- return this.#callbacks.onQuit();
804
- }
805
- const shortcut = this.#shortcuts.find((s) => s.key === key);
806
- if (shortcut) {
807
- shortcut.handler();
808
- }
809
- }
810
- /**
811
- * Handle opening browser with the configured server URL
812
- *
813
- * Uses the 'open' package to launch the default browser and navigate
814
- * to the development server URL.
815
- */
816
- async #handleOpenBrowser() {
817
- this.#logger.log("");
818
- this.#logger.info(`Opening ${this.#serverUrl}...`);
819
- const { default: open } = await import("open");
820
- open(this.#serverUrl);
821
- }
822
- /**
823
- * Show available keyboard shortcuts in the console
824
- *
825
- * Displays a formatted list of all available keyboard shortcuts
826
- * and their descriptions to help users understand what actions
827
- * are available.
828
- */
829
- showHelp() {
830
- this.#logger.log("");
831
- this.#logger.log("Available shortcuts:");
832
- this.#shortcuts.forEach(({ key, description }) => this.#logger.log(`\xB7 ${key}: ${description}`));
833
- }
834
- /**
835
- * Cleanup keyboard shortcuts and restore terminal state
836
- *
837
- * Disables raw mode on stdin, removes event listeners, and restores
838
- * the terminal to its normal state. Should be called when shutting down
839
- * the development server.
840
- */
841
- cleanup() {
842
- if (!process.stdin.isTTY) {
843
- return;
844
- }
845
- process.stdin.setRawMode(false);
846
- process.stdin.pause();
847
- process.stdin.unref();
848
- process.stdin.removeListener("data", this.#keyPressHandler);
849
- this.#keyPressHandler = void 0;
850
- }
270
+ #logger;
271
+ #callbacks;
272
+ #serverUrl;
273
+ #keyPressHandler;
274
+ #shortcuts = [
275
+ {
276
+ key: "r",
277
+ description: "restart server",
278
+ handler: () => {
279
+ this.#logger.log("");
280
+ this.#logger.info("Manual restart triggered...");
281
+ this.#callbacks.onRestart();
282
+ }
283
+ },
284
+ {
285
+ key: "c",
286
+ description: "clear console",
287
+ handler: () => {
288
+ this.#callbacks.onClear();
289
+ this.#logger.info("Console cleared");
290
+ }
291
+ },
292
+ {
293
+ key: "o",
294
+ description: "open in browser",
295
+ handler: () => this.#handleOpenBrowser()
296
+ },
297
+ {
298
+ key: "h",
299
+ description: "show this help",
300
+ handler: () => this.showHelp()
301
+ }
302
+ ];
303
+ constructor(options) {
304
+ this.#logger = options.logger;
305
+ this.#callbacks = options.callbacks;
306
+ }
307
+ setServerUrl(url) {
308
+ this.#serverUrl = url;
309
+ }
310
+ setup() {
311
+ if (!process.stdin.isTTY) return;
312
+ process.stdin.setRawMode(true);
313
+ this.#keyPressHandler = (data) => this.#handleKeyPress(data.toString());
314
+ process.stdin.on("data", this.#keyPressHandler);
315
+ }
316
+ #handleKeyPress(key) {
317
+ if (key === "" || key === "") return this.#callbacks.onQuit();
318
+ const shortcut = this.#shortcuts.find((s) => s.key === key);
319
+ if (shortcut) shortcut.handler();
320
+ }
321
+ async #handleOpenBrowser() {
322
+ this.#logger.log("");
323
+ this.#logger.info(`Opening ${this.#serverUrl}...`);
324
+ const { default: open } = await import("open");
325
+ open(this.#serverUrl);
326
+ }
327
+ showHelp() {
328
+ this.#logger.log("");
329
+ this.#logger.log("Available shortcuts:");
330
+ this.#shortcuts.forEach(({ key, description }) => this.#logger.log(`· ${key}: ${description}`));
331
+ }
332
+ cleanup() {
333
+ if (!process.stdin.isTTY) return;
334
+ process.stdin.setRawMode(false);
335
+ process.stdin.pause();
336
+ process.stdin.unref();
337
+ process.stdin.removeListener("data", this.#keyPressHandler);
338
+ this.#keyPressHandler = void 0;
339
+ }
851
340
  };
852
-
853
- // src/dev_server.ts
854
- var DevServer = class _DevServer {
855
- /**
856
- * Pre-allocated info object for hot-hook change events to avoid repeated object creation
857
- */
858
- static #HOT_HOOK_CHANGE_INFO = {
859
- source: "hot-hook",
860
- fullReload: false,
861
- hotReloaded: false
862
- };
863
- /**
864
- * Pre-allocated info object for hot-hook full reload events
865
- */
866
- static #HOT_HOOK_FULL_RELOAD_INFO = {
867
- source: "hot-hook",
868
- fullReload: true,
869
- hotReloaded: false
870
- };
871
- /**
872
- * Pre-allocated info object for hot-hook invalidation events
873
- */
874
- static #HOT_HOOK_INVALIDATED_INFO = {
875
- source: "hot-hook",
876
- fullReload: false,
877
- hotReloaded: true
878
- };
879
- /**
880
- * Pre-allocated info object for file watcher events
881
- */
882
- static #WATCHER_INFO = {
883
- source: "watcher",
884
- fullReload: true,
885
- hotReloaded: false
886
- };
887
- /**
888
- * External listeners that are invoked when child process
889
- * gets an error or closes
890
- */
891
- #onError;
892
- #onClose;
893
- /**
894
- * The stickyPort is set by the start and the startAndWatch methods
895
- * and we will continue to use that port during restart
896
- */
897
- #stickyPort;
898
- /**
899
- * The mode is set by the start and the startAndWatch methods
900
- */
901
- #mode = "static";
902
- /**
903
- * Reference to chokidar watcher
904
- */
905
- #watcher;
906
- /**
907
- * Reference to the child process
908
- */
909
- #httpServer;
910
- /**
911
- * Keyboard shortcuts manager instance
912
- */
913
- #shortcutsManager;
914
- /**
915
- * Filesystem is used to decide which files to watch or entertain when
916
- * using hot-hook
917
- */
918
- #fileSystem;
919
- /**
920
- * Index generator for managing auto-generated index files
921
- */
922
- #indexGenerator;
923
- /**
924
- * Hooks to execute custom actions during the dev server lifecycle
925
- */
926
- #hooks;
927
- /**
928
- * CLI UI instance for displaying colorful messages and progress information
929
- */
930
- ui = cliui2();
931
- /**
932
- * Restarts the HTTP server and throttle concurrent calls to
933
- * ensure we do not end up with a long loop of restarts
934
- */
935
- #restartHTTPServer = throttle(async () => {
936
- if (this.#httpServer) {
937
- this.#httpServer.removeAllListeners();
938
- this.#httpServer.kill("SIGKILL");
939
- }
940
- await this.#startHTTPServer(this.#stickyPort);
941
- }, "restartHTTPServer");
942
- /**
943
- * Sets up keyboard shortcuts for development server interactions
944
- *
945
- * Initializes the shortcuts manager with callbacks for restarting the server,
946
- * clearing the screen, and quitting the application.
947
- */
948
- #setupKeyboardShortcuts() {
949
- this.#shortcutsManager = new ShortcutsManager({
950
- logger: this.ui.logger,
951
- callbacks: {
952
- onRestart: () => this.#restartHTTPServer(),
953
- onClear: () => this.#clearScreen(),
954
- onQuit: () => this.close()
955
- }
956
- });
957
- this.#shortcutsManager.setup();
958
- }
959
- /**
960
- * Cleanup keyboard shortcuts and restore terminal state
961
- *
962
- * Removes keyboard shortcuts event listeners and restores the terminal
963
- * to its normal state when shutting down the development server.
964
- */
965
- #cleanupKeyboardShortcuts() {
966
- this.#shortcutsManager?.cleanup();
967
- }
968
- /**
969
- * The mode in which the DevServer is running.
970
- */
971
- get mode() {
972
- return this.#mode;
973
- }
974
- /**
975
- * Script file to start the development server
976
- */
977
- scriptFile = "bin/server.ts";
978
- /**
979
- * The current working directory URL
980
- */
981
- cwd;
982
- /**
983
- * File path computed from the cwd
984
- */
985
- cwdPath;
986
- /**
987
- * Development server configuration options including hooks and environment variables
988
- */
989
- options;
990
- /**
991
- * Create a new DevServer instance
992
- *
993
- * @param cwd - The current working directory URL
994
- * @param options - Development server configuration options
995
- */
996
- constructor(cwd, options) {
997
- this.cwd = cwd;
998
- this.options = options;
999
- this.cwdPath = string3.toUnixSlash(fileURLToPath2(this.cwd));
1000
- }
1001
- /**
1002
- * Type guard to check if child process message is from AdonisJS HTTP server
1003
- *
1004
- * Validates that a message from the child process contains the expected
1005
- * structure indicating the AdonisJS server is ready and listening.
1006
- *
1007
- * @param message - Unknown message from child process
1008
- * @returns True if message is an AdonisJS ready message
1009
- */
1010
- #isAdonisJSReadyMessage(message) {
1011
- return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
1012
- }
1013
- /**
1014
- * Displays server information and executes hooks after server startup
1015
- *
1016
- * Shows server URL, mode, startup duration, and help instructions.
1017
- * Also executes the devServerStarted hooks to allow custom post-startup logic.
1018
- *
1019
- * @param message - Server ready message containing port, host, and optional duration
1020
- */
1021
- async #postServerReady(message) {
1022
- const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
1023
- const info = { host, port: message.port };
1024
- const serverUrl = `http://${host}:${message.port}`;
1025
- this.#shortcutsManager?.setServerUrl(serverUrl);
1026
- const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(serverUrl)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
1027
- if (message.duration) {
1028
- displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
1029
- }
1030
- displayMessage.add(`Press ${this.ui.colors.dim("h")} to show help`);
1031
- try {
1032
- await this.#hooks.runner("devServerStarted").run(this, info, displayMessage);
1033
- } catch (error) {
1034
- this.ui.logger.error('One of the "devServerStarted" hooks failed');
1035
- this.ui.logger.fatal(error);
1036
- }
1037
- displayMessage.render();
1038
- }
1039
- /**
1040
- * Type guard to check if child process message is from hot-hook
1041
- *
1042
- * Validates that a message from the child process is a hot-hook notification
1043
- * about file changes, invalidations, or full reloads.
1044
- *
1045
- * @param message - Unknown message from child process
1046
- * @returns True if message is a hot-hook message
1047
- */
1048
- #isHotHookMessage(message) {
1049
- return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
1050
- }
1051
- /**
1052
- * Conditionally clears the terminal screen based on configuration
1053
- *
1054
- * Clears the terminal screen if the clearScreen option is enabled,
1055
- * providing a clean view for development output.
1056
- */
1057
- #clearScreen() {
1058
- if (this.options.clearScreen) {
1059
- process.stdout.write("\x1Bc");
1060
- }
1061
- }
1062
- /**
1063
- * Handles file change events and triggers appropriate server actions
1064
- *
1065
- * Processes file change notifications and determines whether to restart
1066
- * the server, hot reload, or ignore the change based on file type and mode.
1067
- *
1068
- * @param relativePath - Relative path to the changed file
1069
- * @param absolutePath - Absolute path to the changed file
1070
- * @param action - Type of file change (add, update, delete)
1071
- * @param info - Optional information about the change source and reload behavior
1072
- */
1073
- #handleFileChange(relativePath, absolutePath, action, info) {
1074
- if ((action === "add" || action === "delete") && this.mode === "hmr") {
1075
- debug_default("ignoring add and delete actions in HMR mode %s", relativePath);
1076
- return;
1077
- }
1078
- if (info && info.source === "hot-hook" && info.hotReloaded) {
1079
- debug_default("hot reloading %s, info %O", relativePath, info);
1080
- this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${relativePath}`);
1081
- return;
1082
- }
1083
- if (info && !info.fullReload) {
1084
- debug_default("ignoring full reload", relativePath, info);
1085
- return;
1086
- }
1087
- const file = this.#fileSystem.inspect(absolutePath, relativePath);
1088
- if (!file) {
1089
- return;
1090
- }
1091
- if (file.reloadServer) {
1092
- this.#clearScreen();
1093
- this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
1094
- this.#restartHTTPServer();
1095
- return;
1096
- }
1097
- this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
1098
- }
1099
- /**
1100
- * Regenerates index files when a file is added or removed
1101
- *
1102
- * Updates the index generator to reflect file system changes by adding
1103
- * or removing files from the generated index files.
1104
- *
1105
- * @param filePath - Absolute path to the file that changed
1106
- * @param action - Whether the file was added or deleted
1107
- */
1108
- #regenerateIndex(filePath, action) {
1109
- if (action === "add") {
1110
- return this.#indexGenerator.addFile(filePath);
1111
- }
1112
- return this.#indexGenerator.removeFile(filePath);
1113
- }
1114
- /**
1115
- * Registers hooks for file system events and server restart triggers
1116
- *
1117
- * Sets up event handlers that respond to file additions, changes, and removals
1118
- * by regenerating indexes and handling server restarts as needed.
1119
- */
1120
- #registerServerRestartHooks() {
1121
- this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
1122
- this.#regenerateIndex(absolutePath, "add");
1123
- this.#handleFileChange(relativePath, absolutePath, "add");
1124
- });
1125
- this.#hooks.add(
1126
- "fileChanged",
1127
- (relativePath, absolutePath, info) => this.#handleFileChange(relativePath, absolutePath, "update", info)
1128
- );
1129
- this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
1130
- this.#regenerateIndex(absolutePath, "delete");
1131
- this.#handleFileChange(relativePath, absolutePath, "delete");
1132
- });
1133
- }
1134
- /**
1135
- * Initiate the state for DevServer and executes the init hooks
1136
- */
1137
- async #init(ts, mode) {
1138
- const tsConfig = parseConfig(this.cwd, ts);
1139
- if (!tsConfig) {
1140
- this.#onError?.(new RuntimeException("Unable to parse tsconfig file"));
1141
- return false;
1142
- }
1143
- this.#mode = mode;
1144
- this.#clearScreen();
1145
- this.ui.logger.info(`starting server in ${this.#mode} mode...`);
1146
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1147
- this.#stickyPort = String(await getPort(this.cwd));
1148
- this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, this.options);
1149
- this.ui.logger.info("loading hooks...");
1150
- this.#hooks = await loadHooks(this.options.hooks, [
1151
- "init",
1152
- "routesCommitted",
1153
- "routesScanning",
1154
- "routesScanned",
1155
- "devServerStarting",
1156
- "devServerStarted",
1157
- "fileAdded",
1158
- "fileChanged",
1159
- "fileRemoved"
1160
- ]);
1161
- this.#registerServerRestartHooks();
1162
- this.#setupKeyboardShortcuts();
1163
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
1164
- this.#hooks.clear("init");
1165
- this.ui.logger.info("generating indexes...");
1166
- await this.#indexGenerator.generate();
1167
- return true;
1168
- }
1169
- /**
1170
- * Starts the HTTP server as a child process
1171
- *
1172
- * Creates a new Node.js child process to run the server script with the
1173
- * specified port and configuration. Sets up message handlers for server
1174
- * ready notifications and hot-hook events.
1175
- *
1176
- * @param port - Port number for the server to listen on
1177
- */
1178
- async #startHTTPServer(port) {
1179
- await this.#hooks.runner("devServerStarting").run(this);
1180
- debug_default('starting http server using "%s" file, options %O', this.scriptFile, this.options);
1181
- return new Promise(async (resolve) => {
1182
- this.#httpServer = runNode(this.cwd, {
1183
- script: this.scriptFile,
1184
- env: { PORT: port, ...this.options.env },
1185
- nodeArgs: this.options.nodeArgs,
1186
- reject: true,
1187
- scriptArgs: this.options.scriptArgs
1188
- });
1189
- this.#httpServer.on("message", async (message) => {
1190
- if (this.#isAdonisJSReadyMessage(message)) {
1191
- debug_default("received http server ready message %O", message);
1192
- await this.#postServerReady(message);
1193
- resolve();
1194
- } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
1195
- debug_default("received hot-hook message %O", message);
1196
- const absolutePath = message.path ? string3.toUnixSlash(message.path) : "";
1197
- const relativePath = relative3(this.cwdPath, absolutePath);
1198
- if (message.type === "hot-hook:file-changed") {
1199
- const { action } = message;
1200
- if (action === "add") {
1201
- this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1202
- } else if (action === "change") {
1203
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_CHANGE_INFO, this);
1204
- } else if (action === "unlink") {
1205
- this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1206
- }
1207
- } else if (message.type === "hot-hook:full-reload") {
1208
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_FULL_RELOAD_INFO, this);
1209
- } else if (message.type === "hot-hook:invalidated") {
1210
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_INVALIDATED_INFO, this);
1211
- }
1212
- }
1213
- });
1214
- this.#httpServer.then((result) => {
1215
- if (!this.#watcher) {
1216
- this.#onClose?.(result.exitCode);
1217
- } else {
1218
- this.ui.logger.info("Underlying HTTP server closed. Still watching for changes");
1219
- }
1220
- }).catch((error) => {
1221
- if (!this.#watcher) {
1222
- this.#onError?.(error);
1223
- } else {
1224
- this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
1225
- }
1226
- }).finally(() => {
1227
- resolve();
1228
- });
1229
- });
1230
- }
1231
- /**
1232
- * Add listener to get notified when dev server is closed
1233
- *
1234
- * @param callback - Function to call when dev server closes
1235
- * @returns This DevServer instance for method chaining
1236
- */
1237
- onClose(callback) {
1238
- this.#onClose = callback;
1239
- return this;
1240
- }
1241
- /**
1242
- * Add listener to get notified when dev server encounters an error
1243
- *
1244
- * @param callback - Function to call when dev server encounters an error
1245
- * @returns This DevServer instance for method chaining
1246
- */
1247
- onError(callback) {
1248
- this.#onError = callback;
1249
- return this;
1250
- }
1251
- /**
1252
- * Close watchers and the running child process
1253
- */
1254
- async close() {
1255
- this.#cleanupKeyboardShortcuts();
1256
- await this.#watcher?.close();
1257
- if (this.#httpServer) {
1258
- this.#httpServer.removeAllListeners();
1259
- this.#httpServer.kill("SIGKILL");
1260
- }
1261
- }
1262
- /**
1263
- * Start the development server in static or HMR mode
1264
- *
1265
- * @param ts - TypeScript module reference
1266
- */
1267
- async start(ts) {
1268
- const initiated = await this.#init(ts, this.options.hmr ? "hmr" : "static");
1269
- if (!initiated) {
1270
- return;
1271
- }
1272
- if (this.#mode === "hmr") {
1273
- this.options.nodeArgs.push("--import=hot-hook/register");
1274
- this.options.env = {
1275
- ...this.options.env,
1276
- HOT_HOOK_INCLUDE: this.#fileSystem.includes.join(","),
1277
- HOT_HOOK_IGNORE: this.#fileSystem.excludes.join(","),
1278
- HOT_HOOK_RESTART: (this.options.metaFiles ?? []).filter(({ reloadServer }) => !!reloadServer).map(({ pattern }) => pattern).join(",")
1279
- };
1280
- }
1281
- this.ui.logger.info("starting HTTP server...");
1282
- await this.#startHTTPServer(this.#stickyPort);
1283
- }
1284
- /**
1285
- * Start the development server in watch mode and restart on file changes
1286
- *
1287
- * @param ts - TypeScript module reference
1288
- * @param options - Watch options including polling mode
1289
- */
1290
- async startAndWatch(ts, options) {
1291
- const initiated = await this.#init(ts, "watch");
1292
- if (!initiated) {
1293
- return;
1294
- }
1295
- this.ui.logger.info("starting HTTP server...");
1296
- await this.#startHTTPServer(this.#stickyPort);
1297
- this.#watcher = watch({
1298
- usePolling: options?.poll ?? false,
1299
- cwd: this.cwdPath,
1300
- ignoreInitial: true,
1301
- ignored: (file, stats) => {
1302
- if (!stats) {
1303
- return false;
1304
- }
1305
- if (stats.isFile()) {
1306
- return !this.#fileSystem.shouldWatchFile(file);
1307
- }
1308
- return !this.#fileSystem.shouldWatchDirectory(file);
1309
- }
1310
- });
1311
- this.#watcher.on("ready", () => {
1312
- this.ui.logger.info("watching file system for changes...");
1313
- });
1314
- this.#watcher.on("error", (error) => {
1315
- this.ui.logger.warning("file system watcher failure");
1316
- this.ui.logger.fatal(error);
1317
- this.#onError?.(error);
1318
- this.#watcher?.close();
1319
- });
1320
- this.#watcher.on("add", (filePath) => {
1321
- const relativePath = string3.toUnixSlash(filePath);
1322
- const absolutePath = join2(this.cwdPath, relativePath);
1323
- this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1324
- });
1325
- this.#watcher.on("change", (filePath) => {
1326
- const relativePath = string3.toUnixSlash(filePath);
1327
- const absolutePath = join2(this.cwdPath, relativePath);
1328
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#WATCHER_INFO, this);
1329
- });
1330
- this.#watcher.on("unlink", (filePath) => {
1331
- const relativePath = string3.toUnixSlash(filePath);
1332
- const absolutePath = join2(this.cwdPath, relativePath);
1333
- this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1334
- });
1335
- }
341
+ var DevServer = class DevServer {
342
+ static #HOT_HOOK_FULL_RELOAD_INFO = {
343
+ source: "hot-hook",
344
+ fullReload: true,
345
+ hotReloaded: false
346
+ };
347
+ static #HOT_HOOK_INVALIDATED_INFO = {
348
+ source: "hot-hook",
349
+ fullReload: false,
350
+ hotReloaded: true
351
+ };
352
+ static #WATCHER_INFO = {
353
+ source: "watcher",
354
+ fullReload: true,
355
+ hotReloaded: false
356
+ };
357
+ #onError;
358
+ #onClose;
359
+ #stickyPort;
360
+ #stickyHmrPort;
361
+ #mode = "static";
362
+ #watcher;
363
+ #httpServer;
364
+ #isHttpServerAlive = false;
365
+ #shortcutsManager;
366
+ #fileSystem;
367
+ #indexGenerator;
368
+ #routesScanner;
369
+ #hooks;
370
+ ui = cliui();
371
+ #restartHTTPServer = throttle(async () => {
372
+ if (this.#httpServer) {
373
+ this.#httpServer.removeAllListeners();
374
+ this.#httpServer.kill("SIGKILL");
375
+ }
376
+ await this.#startHTTPServer(this.#stickyPort, this.#stickyHmrPort);
377
+ }, "restartHTTPServer");
378
+ #setupKeyboardShortcuts() {
379
+ this.#shortcutsManager = new ShortcutsManager({
380
+ logger: this.ui.logger,
381
+ callbacks: {
382
+ onRestart: () => this.#restartHTTPServer(),
383
+ onClear: () => this.#clearScreen(),
384
+ onQuit: () => this.close()
385
+ }
386
+ });
387
+ this.#shortcutsManager.setup();
388
+ }
389
+ #cleanupKeyboardShortcuts() {
390
+ this.#shortcutsManager?.cleanup();
391
+ }
392
+ get mode() {
393
+ return this.#mode;
394
+ }
395
+ scriptFile = "bin/server.ts";
396
+ cwd;
397
+ cwdPath;
398
+ options;
399
+ constructor(cwd, options) {
400
+ this.cwd = cwd;
401
+ this.options = options;
402
+ this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
403
+ }
404
+ #isAdonisJSReadyMessage(message) {
405
+ return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
406
+ }
407
+ #isAdonisJSRoutesMessage(message) {
408
+ return message !== null && typeof message === "object" && "routesFileLocation" in message;
409
+ }
410
+ async #postServerReady(message) {
411
+ const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
412
+ const info = {
413
+ host,
414
+ port: message.port
415
+ };
416
+ const serverUrl = `http://${host}:${message.port}`;
417
+ this.#shortcutsManager?.setServerUrl(serverUrl);
418
+ const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(serverUrl)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
419
+ if (message.duration) displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
420
+ displayMessage.add(`Press ${this.ui.colors.dim("h")} to show help`);
421
+ try {
422
+ await this.#hooks.runner("devServerStarted").run(this, info, displayMessage);
423
+ } catch (error) {
424
+ this.ui.logger.error("One of the \"devServerStarted\" hooks failed");
425
+ this.ui.logger.fatal(error);
426
+ }
427
+ displayMessage.render();
428
+ }
429
+ #isHotHookMessage(message) {
430
+ return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
431
+ }
432
+ #clearScreen() {
433
+ if (this.options.clearScreen) process.stdout.write("\x1Bc");
434
+ }
435
+ #createWatcher(options) {
436
+ const watcher = watch({
437
+ usePolling: options?.poll ?? false,
438
+ cwd: this.cwdPath,
439
+ ignoreInitial: true,
440
+ ignored: (file, stats) => {
441
+ if (!stats) return false;
442
+ if (file.includes("inertia") && !file.includes("node_modules")) return false;
443
+ if (stats.isFile()) return !this.#fileSystem.shouldWatchFile(file);
444
+ return !this.#fileSystem.shouldWatchDirectory(file);
445
+ }
446
+ });
447
+ watcher.on("error", (error) => {
448
+ this.ui.logger.warning("file system watcher failure");
449
+ this.ui.logger.fatal(error);
450
+ this.#onError?.(error);
451
+ this.#watcher?.close();
452
+ });
453
+ watcher.on("ready", () => {
454
+ this.ui.logger.info("watching file system for changes...");
455
+ });
456
+ return watcher;
457
+ }
458
+ #handleHmrWatcherEvent(options) {
459
+ const relativePath = string.toUnixSlash(options.filePath);
460
+ const absolutePath = join(this.cwdPath, relativePath);
461
+ if (this.#isHttpServerAlive === false) {
462
+ this.#clearScreen();
463
+ this.ui.logger.log(`${this.ui.colors.green(options.displayLabel)} ${relativePath}`);
464
+ this.#restartHTTPServer();
465
+ return;
466
+ }
467
+ if (options.action === "add") this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
468
+ else if (options.action === "unlink") this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
469
+ this.#httpServer?.send({
470
+ type: "hot-hook:file-changed",
471
+ path: absolutePath,
472
+ action: options.action
473
+ });
474
+ }
475
+ #handleFileChange(relativePath, absolutePath, action, info) {
476
+ if ((action === "add" || action === "delete") && this.mode === "hmr") {
477
+ debug_default("ignoring add and delete actions in HMR mode %s", relativePath);
478
+ return;
479
+ }
480
+ if (info && info.source === "hot-hook" && info.hotReloaded) {
481
+ debug_default("hot reloading %s, info %O", relativePath, info);
482
+ this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${relativePath}`);
483
+ return;
484
+ }
485
+ if (info && !info.fullReload) {
486
+ debug_default("ignoring full reload", relativePath, info);
487
+ return;
488
+ }
489
+ const file = this.#fileSystem.inspect(absolutePath, relativePath);
490
+ if (!file) return;
491
+ if (file.reloadServer) {
492
+ this.#clearScreen();
493
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
494
+ this.#restartHTTPServer();
495
+ return;
496
+ }
497
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
498
+ }
499
+ #regenerateIndex(filePath, action) {
500
+ if (action === "add") return this.#indexGenerator.addFile(filePath);
501
+ return this.#indexGenerator.removeFile(filePath);
502
+ }
503
+ async #reScanRoutes(filePath) {
504
+ if (!this.#routesScanner) return;
505
+ try {
506
+ if (await this.#routesScanner.invalidate(filePath)) await this.#hooks.runner("routesScanned").run(this, this.#routesScanner);
507
+ } catch (error) {
508
+ this.ui.logger.error("Unable to rescan routes because of the following error");
509
+ this.ui.logger.fatal(error);
510
+ }
511
+ }
512
+ #processRoutes = throttle(async (routesFileLocation) => {
513
+ try {
514
+ const scanRoutes = this.#hooks.has("routesScanning") || this.#hooks.has("routesScanned");
515
+ const shareRoutes = this.#hooks.has("routesCommitted");
516
+ if (!scanRoutes && !shareRoutes) {
517
+ unlink(routesFileLocation).catch(() => {});
518
+ return;
519
+ }
520
+ const routesJSON = await readFile(routesFileLocation, "utf-8");
521
+ const routesList = JSON.parse(routesJSON);
522
+ unlink(routesFileLocation).catch(() => {});
523
+ if (shareRoutes) await this.#hooks.runner("routesCommitted").run(this, routesList);
524
+ if (scanRoutes) {
525
+ this.#routesScanner = new RoutesScanner(this.cwdPath, []);
526
+ await this.#hooks.runner("routesScanning").run(this, this.#routesScanner);
527
+ for (const domain of Object.keys(routesList)) await this.#routesScanner.scan(routesList[domain]);
528
+ await this.#hooks.runner("routesScanned").run(this, this.#routesScanner);
529
+ }
530
+ } catch (error) {
531
+ this.ui.logger.error("Unable to process and scan routes because of the following error");
532
+ this.ui.logger.fatal(error);
533
+ }
534
+ }, "processRoutes");
535
+ #registerServerRestartHooks() {
536
+ this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
537
+ this.#regenerateIndex(absolutePath, "add");
538
+ this.#handleFileChange(relativePath, absolutePath, "add");
539
+ });
540
+ this.#hooks.add("fileChanged", (relativePath, absolutePath, info) => {
541
+ if (info.hotReloaded || !info.hotReloaded && !info.fullReload) this.#reScanRoutes(absolutePath);
542
+ this.#handleFileChange(relativePath, absolutePath, "update", info);
543
+ });
544
+ this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
545
+ this.#regenerateIndex(absolutePath, "delete");
546
+ this.#handleFileChange(relativePath, absolutePath, "delete");
547
+ });
548
+ }
549
+ async #init(mode) {
550
+ const tsConfig = readTsConfig(this.cwdPath);
551
+ if (!tsConfig) {
552
+ this.#onError?.(new RuntimeException("Unable to parse tsconfig file"));
553
+ return false;
554
+ }
555
+ this.#mode = mode;
556
+ this.#clearScreen();
557
+ this.ui.logger.info(`starting server in ${this.#mode} mode...`);
558
+ this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
559
+ this.#stickyPort = String(await getPort(this.cwd));
560
+ this.#stickyHmrPort = String(await getRandomPort({ port: 24678 }));
561
+ this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, this.options);
562
+ this.ui.logger.info("loading hooks...");
563
+ this.#hooks = await loadHooks(this.options.hooks, [
564
+ "init",
565
+ "routesCommitted",
566
+ "routesScanning",
567
+ "routesScanned",
568
+ "devServerStarting",
569
+ "devServerStarted",
570
+ "fileAdded",
571
+ "fileChanged",
572
+ "fileRemoved"
573
+ ]);
574
+ this.#registerServerRestartHooks();
575
+ this.#setupKeyboardShortcuts();
576
+ await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
577
+ this.#hooks.clear("init");
578
+ this.ui.logger.info("generating indexes...");
579
+ await this.#indexGenerator.generate();
580
+ return true;
581
+ }
582
+ async #startHTTPServer(port, hmrPort) {
583
+ await this.#hooks.runner("devServerStarting").run(this);
584
+ debug_default("starting http server using \"%s\" file, options %O", this.scriptFile, this.options);
585
+ return new Promise((resolve) => {
586
+ this.#httpServer = runNode(this.cwd, {
587
+ script: this.scriptFile,
588
+ env: {
589
+ PORT: port,
590
+ VITE_HMR_PORT: hmrPort,
591
+ DEV_MODE: "true",
592
+ ...this.options.env
593
+ },
594
+ nodeArgs: this.options.nodeArgs,
595
+ reject: true,
596
+ scriptArgs: this.options.scriptArgs
597
+ });
598
+ this.#isHttpServerAlive = true;
599
+ this.#httpServer.on("message", async (message) => {
600
+ if (this.#isAdonisJSReadyMessage(message)) {
601
+ debug_default("received http server ready message %O", message);
602
+ await this.#postServerReady(message);
603
+ resolve();
604
+ } else if (this.#isAdonisJSRoutesMessage(message)) {
605
+ debug_default("received routes location from the server %O", message);
606
+ await this.#processRoutes(message.routesFileLocation);
607
+ } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
608
+ debug_default("received hot-hook message %O", message);
609
+ if (message.type === "hot-hook:full-reload") {
610
+ const absolutePath = message.path ? string.toUnixSlash(message.path) : "";
611
+ const relativePath = relative(this.cwdPath, absolutePath);
612
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, DevServer.#HOT_HOOK_FULL_RELOAD_INFO, this);
613
+ } else if (message.type === "hot-hook:invalidated") {
614
+ const absolutePath = message.paths[0] ? string.toUnixSlash(message.paths[0]) : "";
615
+ const relativePath = relative(this.cwdPath, absolutePath);
616
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, DevServer.#HOT_HOOK_INVALIDATED_INFO, this);
617
+ }
618
+ }
619
+ });
620
+ this.#httpServer.then((result) => {
621
+ this.#isHttpServerAlive = false;
622
+ if (!this.#watcher) this.#onClose?.(result.exitCode);
623
+ else this.ui.logger.info("Underlying HTTP server closed. Still watching for changes");
624
+ }).catch((error) => {
625
+ this.#isHttpServerAlive = false;
626
+ if (!this.#watcher) this.#onError?.(error);
627
+ else this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
628
+ }).finally(() => {
629
+ resolve();
630
+ });
631
+ });
632
+ }
633
+ onClose(callback) {
634
+ this.#onClose = callback;
635
+ return this;
636
+ }
637
+ onError(callback) {
638
+ this.#onError = callback;
639
+ return this;
640
+ }
641
+ async close() {
642
+ this.#cleanupKeyboardShortcuts();
643
+ await this.#watcher?.close();
644
+ if (this.#httpServer) {
645
+ this.#httpServer.removeAllListeners();
646
+ this.#httpServer.kill("SIGKILL");
647
+ }
648
+ }
649
+ async start() {
650
+ if (!await this.#init(this.options.hmr ? "hmr" : "static")) return;
651
+ if (this.#mode === "hmr") {
652
+ this.options.nodeArgs.push("--import=hot-hook/register");
653
+ this.options.env = {
654
+ ...this.options.env,
655
+ HOT_HOOK_WATCH: "false"
656
+ };
657
+ }
658
+ this.ui.logger.info("starting HTTP server...");
659
+ await this.#startHTTPServer(this.#stickyPort, this.#stickyHmrPort);
660
+ if (this.#mode !== "hmr") return;
661
+ this.#watcher = this.#createWatcher();
662
+ this.#watcher.on("add", (filePath) => {
663
+ this.#handleHmrWatcherEvent({
664
+ filePath,
665
+ action: "add",
666
+ displayLabel: "add"
667
+ });
668
+ });
669
+ this.#watcher.on("change", (filePath) => {
670
+ this.#handleHmrWatcherEvent({
671
+ filePath,
672
+ action: "change",
673
+ displayLabel: "update"
674
+ });
675
+ });
676
+ this.#watcher.on("unlink", (filePath) => {
677
+ this.#handleHmrWatcherEvent({
678
+ filePath,
679
+ action: "unlink",
680
+ displayLabel: "delete"
681
+ });
682
+ });
683
+ }
684
+ async startAndWatch(options) {
685
+ if (!await this.#init("watch")) return;
686
+ this.ui.logger.info("starting HTTP server...");
687
+ await this.#startHTTPServer(this.#stickyPort, this.#stickyHmrPort);
688
+ this.#watcher = this.#createWatcher({ poll: options?.poll });
689
+ this.#watcher.on("add", (filePath) => {
690
+ const relativePath = string.toUnixSlash(filePath);
691
+ const absolutePath = join(this.cwdPath, relativePath);
692
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
693
+ });
694
+ this.#watcher.on("change", (filePath) => {
695
+ const relativePath = string.toUnixSlash(filePath);
696
+ const absolutePath = join(this.cwdPath, relativePath);
697
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, DevServer.#WATCHER_INFO, this);
698
+ });
699
+ this.#watcher.on("unlink", (filePath) => {
700
+ const relativePath = string.toUnixSlash(filePath);
701
+ const absolutePath = join(this.cwdPath, relativePath);
702
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
703
+ });
704
+ }
1336
705
  };
1337
-
1338
- // src/test_runner.ts
1339
- import { join as join3 } from "path/posix";
1340
- import { cliui as cliui3 } from "@poppinss/cliui";
1341
- import { fileURLToPath as fileURLToPath3 } from "url";
1342
- import string4 from "@poppinss/utils/string";
1343
- import { RuntimeException as RuntimeException2 } from "@poppinss/utils/exception";
1344
706
  var TestRunner = class {
1345
- /**
1346
- * External listeners that are invoked when child process
1347
- * gets an error or closes
1348
- */
1349
- #onError;
1350
- #onClose;
1351
- /**
1352
- * The stickyPort is set by the startAndWatch method and we will
1353
- * continue to use this port during re-runs
1354
- */
1355
- #stickyPort;
1356
- /**
1357
- * Reference to chokidar watcher
1358
- */
1359
- #watcher;
1360
- /**
1361
- * Reference to the test script child process
1362
- */
1363
- #testsProcess;
1364
- /**
1365
- * Filesystem is used to decide which files to watch or entertain in watch
1366
- * mode
1367
- */
1368
- #fileSystem;
1369
- /**
1370
- * Hooks to execute custom actions during the tests runner lifecycle
1371
- */
1372
- #hooks;
1373
- /**
1374
- * Index generator for managing auto-generated index files
1375
- */
1376
- #indexGenerator;
1377
- /**
1378
- * CLI UI instance for displaying colorful messages and progress information
1379
- */
1380
- ui = cliui3();
1381
- /**
1382
- * Re-runs the test child process and throttle concurrent calls to
1383
- * ensure we do not end up with a long loop of restarts
1384
- */
1385
- #reRunTests = throttle(async (filters) => {
1386
- if (this.#testsProcess) {
1387
- this.#testsProcess.removeAllListeners();
1388
- this.#testsProcess.kill("SIGKILL");
1389
- }
1390
- await this.#runTests(this.#stickyPort, filters);
1391
- }, "reRunTests");
1392
- /**
1393
- * The script file to run as a child process
1394
- */
1395
- scriptFile = "bin/test.ts";
1396
- /**
1397
- * The current working directory URL
1398
- */
1399
- cwd;
1400
- /**
1401
- * The current working directory path as a string
1402
- */
1403
- cwdPath;
1404
- /**
1405
- * Test runner configuration options including filters, reporters, and hooks
1406
- */
1407
- options;
1408
- /**
1409
- * Create a new TestRunner instance
1410
- *
1411
- * @param cwd - The current working directory URL
1412
- * @param options - Test runner configuration options
1413
- */
1414
- constructor(cwd, options) {
1415
- this.cwd = cwd;
1416
- this.options = options;
1417
- this.cwdPath = string4.toUnixSlash(fileURLToPath3(this.cwd));
1418
- }
1419
- /**
1420
- * Convert test runner options to the CLI args
1421
- *
1422
- * Transforms the test runner configuration options into command-line
1423
- * arguments that can be passed to the test script.
1424
- *
1425
- * @returns Array of command-line arguments
1426
- */
1427
- #convertOptionsToArgs() {
1428
- const args = [];
1429
- if (this.options.reporters) {
1430
- args.push("--reporters");
1431
- args.push(this.options.reporters.join(","));
1432
- }
1433
- if (this.options.timeout !== void 0) {
1434
- args.push("--timeout");
1435
- args.push(String(this.options.timeout));
1436
- }
1437
- if (this.options.failed) {
1438
- args.push("--failed");
1439
- }
1440
- if (this.options.retries !== void 0) {
1441
- args.push("--retries");
1442
- args.push(String(this.options.retries));
1443
- }
1444
- return args;
1445
- }
1446
- /**
1447
- * Converts all known filters to CLI args
1448
- *
1449
- * Transforms test filters (suites, files, groups, tags, tests) into
1450
- * command-line arguments for the test script.
1451
- *
1452
- * @param filters - The test filters to convert
1453
- * @returns Array of command-line arguments representing the filters
1454
- */
1455
- #convertFiltersToArgs(filters) {
1456
- const args = [];
1457
- if (filters.suites) {
1458
- args.push(...filters.suites);
1459
- }
1460
- if (filters.files) {
1461
- args.push("--files");
1462
- args.push(filters.files.join(","));
1463
- }
1464
- if (filters.groups) {
1465
- args.push("--groups");
1466
- args.push(filters.groups.join(","));
1467
- }
1468
- if (filters.tags) {
1469
- args.push("--tags");
1470
- args.push(filters.tags.join(","));
1471
- }
1472
- if (filters.tests) {
1473
- args.push("--tests");
1474
- args.push(filters.tests.join(","));
1475
- }
1476
- return args;
1477
- }
1478
- /**
1479
- * Conditionally clear the terminal screen
1480
- *
1481
- * Clears the terminal screen if the clearScreen option is enabled
1482
- * in the test runner configuration.
1483
- */
1484
- #clearScreen() {
1485
- if (this.options.clearScreen) {
1486
- process.stdout.write("\x1Bc");
1487
- }
1488
- }
1489
- /**
1490
- * Runs tests as a child process
1491
- *
1492
- * Creates a Node.js child process to execute the test script with
1493
- * appropriate command-line arguments and environment variables.
1494
- * Handles process lifecycle and hook execution.
1495
- *
1496
- * @param port - The port number to set in the environment
1497
- * @param filters - Optional test filters to apply for this run
1498
- */
1499
- async #runTests(port, filters) {
1500
- await this.#hooks.runner("testsStarting").run(this);
1501
- debug_default('running tests using "%s" file, options %O', this.scriptFile, this.options);
1502
- return new Promise(async (resolve) => {
1503
- const mergedFilters = { ...this.options.filters, ...filters };
1504
- const scriptArgs = [
1505
- ...this.#convertOptionsToArgs(),
1506
- ...this.options.scriptArgs,
1507
- ...this.#convertFiltersToArgs(mergedFilters)
1508
- ];
1509
- this.#testsProcess = runNode(this.cwd, {
1510
- script: this.scriptFile,
1511
- reject: true,
1512
- env: { PORT: port, ...this.options.env },
1513
- nodeArgs: this.options.nodeArgs,
1514
- scriptArgs
1515
- });
1516
- this.#testsProcess.then((result) => {
1517
- this.#hooks.runner("testsFinished").run(this).catch((error) => {
1518
- this.ui.logger.error('One of the "testsFinished" hooks failed');
1519
- this.ui.logger.fatal(error);
1520
- }).finally(() => {
1521
- if (!this.#watcher) {
1522
- this.#onClose?.(result.exitCode);
1523
- this.close();
1524
- }
1525
- });
1526
- }).catch((error) => {
1527
- if (!this.#watcher) {
1528
- this.#onError?.(error);
1529
- this.close();
1530
- } else {
1531
- this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
1532
- }
1533
- }).finally(() => resolve());
1534
- });
1535
- }
1536
- /**
1537
- * Handles file change event during watch mode
1538
- *
1539
- * Determines whether to run specific tests or all tests based on
1540
- * the type of file that changed. Test files trigger selective runs,
1541
- * while other files trigger full test suite runs.
1542
- *
1543
- * @param filePath - The path of the changed file
1544
- * @param action - The type of change (add, update, delete)
1545
- */
1546
- #handleFileChange(relativePath, absolutePath, action) {
1547
- const file = this.#fileSystem.inspect(absolutePath, relativePath);
1548
- if (!file) {
1549
- return;
1550
- }
1551
- this.#clearScreen();
1552
- this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
1553
- if (file.fileType === "test") {
1554
- this.#reRunTests({ files: [relativePath] });
1555
- } else {
1556
- this.#reRunTests();
1557
- }
1558
- }
1559
- /**
1560
- * Re-generates the index when a file is changed, but only in HMR
1561
- * mode
1562
- */
1563
- #regenerateIndex(filePath, action) {
1564
- if (action === "add") {
1565
- return this.#indexGenerator.addFile(filePath);
1566
- }
1567
- return this.#indexGenerator.removeFile(filePath);
1568
- }
1569
- /**
1570
- * Registers inline hooks for file changes and test re-runs
1571
- *
1572
- * Sets up event handlers that respond to file system changes by
1573
- * triggering appropriate test runs based on the changed files.
1574
- */
1575
- #registerServerRestartHooks() {
1576
- this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
1577
- this.#regenerateIndex(absolutePath, "add");
1578
- this.#handleFileChange(relativePath, absolutePath, "add");
1579
- });
1580
- this.#hooks.add("fileChanged", (relativePath, absolutePath) => {
1581
- this.#regenerateIndex(absolutePath, "add");
1582
- this.#handleFileChange(relativePath, absolutePath, "update");
1583
- });
1584
- this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
1585
- this.#regenerateIndex(absolutePath, "delete");
1586
- this.#handleFileChange(relativePath, absolutePath, "delete");
1587
- });
1588
- }
1589
- /**
1590
- * Add listener to get notified when test runner is closed
1591
- *
1592
- * @param callback - Function to call when test runner closes
1593
- * @returns This TestRunner instance for method chaining
1594
- */
1595
- onClose(callback) {
1596
- this.#onClose = callback;
1597
- return this;
1598
- }
1599
- /**
1600
- * Add listener to get notified when test runner encounters an error
1601
- *
1602
- * @param callback - Function to call when test runner encounters an error
1603
- * @returns This TestRunner instance for method chaining
1604
- */
1605
- onError(callback) {
1606
- this.#onError = callback;
1607
- return this;
1608
- }
1609
- /**
1610
- * Close watchers and running child processes
1611
- *
1612
- * Cleans up file system watchers and terminates any running test
1613
- * processes to ensure graceful shutdown.
1614
- */
1615
- async close() {
1616
- await this.#watcher?.close();
1617
- if (this.#testsProcess) {
1618
- this.#testsProcess.removeAllListeners();
1619
- this.#testsProcess.kill("SIGKILL");
1620
- }
1621
- }
1622
- /**
1623
- * Runs tests once without watching for file changes
1624
- *
1625
- * Executes the test suite a single time and exits. This is the
1626
- * equivalent of running tests in CI/CD environments.
1627
- */
1628
- async run() {
1629
- this.#stickyPort = String(await getPort(this.cwd));
1630
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1631
- this.#clearScreen();
1632
- this.ui.logger.info("loading hooks...");
1633
- this.#hooks = await loadHooks(this.options.hooks, ["init", "testsStarting", "testsFinished"]);
1634
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
1635
- this.#hooks.clear("init");
1636
- this.ui.logger.info("generating indexes...");
1637
- await this.#indexGenerator.generate();
1638
- this.ui.logger.info("booting application to run tests...");
1639
- await this.#runTests(this.#stickyPort);
1640
- }
1641
- /**
1642
- * Run tests in watch mode and re-run them when files change
1643
- *
1644
- * Starts the test runner in watch mode, monitoring the file system
1645
- * for changes and automatically re-running tests when relevant files
1646
- * are modified. Uses intelligent filtering to run only affected tests
1647
- * when possible.
1648
- *
1649
- * @param ts - TypeScript module reference for parsing configuration
1650
- * @param options - Watch options including polling mode for file system monitoring
1651
- */
1652
- async runAndWatch(ts, options) {
1653
- const tsConfig = parseConfig(this.cwd, ts);
1654
- if (!tsConfig) {
1655
- this.#onError?.(new RuntimeException2("Unable to parse tsconfig file"));
1656
- return;
1657
- }
1658
- this.#stickyPort = String(await getPort(this.cwd));
1659
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1660
- this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, {
1661
- ...this.options,
1662
- suites: this.options.suites?.filter((suite) => {
1663
- if (this.options.filters.suites) {
1664
- return this.options.filters.suites.includes(suite.name);
1665
- }
1666
- return true;
1667
- })
1668
- });
1669
- this.#clearScreen();
1670
- this.ui.logger.info("loading hooks...");
1671
- this.#hooks = await loadHooks(this.options.hooks, [
1672
- "init",
1673
- "testsStarting",
1674
- "testsFinished",
1675
- "fileAdded",
1676
- "fileChanged",
1677
- "fileRemoved"
1678
- ]);
1679
- this.#registerServerRestartHooks();
1680
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
1681
- this.#hooks.clear("init");
1682
- this.ui.logger.info("generating indexes...");
1683
- await this.#indexGenerator.generate();
1684
- this.ui.logger.info("booting application to run tests...");
1685
- await this.#runTests(this.#stickyPort);
1686
- this.#watcher = watch({
1687
- usePolling: options?.poll ?? false,
1688
- cwd: this.cwdPath,
1689
- ignoreInitial: true,
1690
- ignored: (file, stats) => {
1691
- if (!stats) {
1692
- return false;
1693
- }
1694
- if (stats.isFile()) {
1695
- return !this.#fileSystem.shouldWatchFile(file);
1696
- }
1697
- return !this.#fileSystem.shouldWatchDirectory(file);
1698
- }
1699
- });
1700
- this.#watcher.on("ready", () => {
1701
- this.ui.logger.info("watching file system for changes...");
1702
- });
1703
- this.#watcher.on("error", (error) => {
1704
- this.ui.logger.warning("file system watcher failure");
1705
- this.ui.logger.fatal(error);
1706
- this.#onError?.(error);
1707
- this.#watcher?.close();
1708
- });
1709
- this.#watcher.on("add", (filePath) => {
1710
- const relativePath = string4.toUnixSlash(filePath);
1711
- const absolutePath = join3(this.cwdPath, filePath);
1712
- this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1713
- });
1714
- this.#watcher.on("change", (filePath) => {
1715
- const relativePath = string4.toUnixSlash(filePath);
1716
- const absolutePath = join3(this.cwdPath, filePath);
1717
- this.#hooks.runner("fileChanged").run(
1718
- relativePath,
1719
- absolutePath,
1720
- {
1721
- source: "watcher",
1722
- fullReload: true,
1723
- hotReloaded: false
1724
- },
1725
- this
1726
- );
1727
- });
1728
- this.#watcher.on("unlink", (filePath) => {
1729
- const relativePath = string4.toUnixSlash(filePath);
1730
- const absolutePath = join3(this.cwdPath, filePath);
1731
- this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1732
- });
1733
- }
1734
- };
1735
- export {
1736
- Bundler,
1737
- DevServer,
1738
- SUPPORTED_PACKAGE_MANAGERS,
1739
- TestRunner,
1740
- hooks
707
+ #onError;
708
+ #onClose;
709
+ #stickyPort;
710
+ #stickyHmrPort;
711
+ #watcher;
712
+ #testsProcess;
713
+ #fileSystem;
714
+ #hooks;
715
+ #indexGenerator;
716
+ ui = cliui();
717
+ #reRunTests = throttle(async (filters) => {
718
+ if (this.#testsProcess) {
719
+ this.#testsProcess.removeAllListeners();
720
+ this.#testsProcess.kill("SIGKILL");
721
+ }
722
+ await this.#runTests(this.#stickyPort, this.#stickyHmrPort, filters);
723
+ }, "reRunTests");
724
+ scriptFile = "bin/test.ts";
725
+ cwd;
726
+ cwdPath;
727
+ options;
728
+ constructor(cwd, options) {
729
+ this.cwd = cwd;
730
+ this.options = options;
731
+ this.cwdPath = string.toUnixSlash(fileURLToPath(this.cwd));
732
+ }
733
+ #convertOptionsToArgs() {
734
+ const args = [];
735
+ if (this.options.reporters) {
736
+ args.push("--reporters");
737
+ args.push(this.options.reporters.join(","));
738
+ }
739
+ if (this.options.timeout !== void 0) {
740
+ args.push("--timeout");
741
+ args.push(String(this.options.timeout));
742
+ }
743
+ if (this.options.failed) args.push("--failed");
744
+ if (this.options.retries !== void 0) {
745
+ args.push("--retries");
746
+ args.push(String(this.options.retries));
747
+ }
748
+ return args;
749
+ }
750
+ #convertFiltersToArgs(filters) {
751
+ const args = [];
752
+ if (filters.suites) args.push(...filters.suites);
753
+ if (filters.files) {
754
+ args.push("--files");
755
+ args.push(filters.files.join(","));
756
+ }
757
+ if (filters.groups) {
758
+ args.push("--groups");
759
+ args.push(filters.groups.join(","));
760
+ }
761
+ if (filters.tags) {
762
+ args.push("--tags");
763
+ args.push(filters.tags.join(","));
764
+ }
765
+ if (filters.tests) {
766
+ args.push("--tests");
767
+ args.push(filters.tests.join(","));
768
+ }
769
+ return args;
770
+ }
771
+ #clearScreen() {
772
+ if (this.options.clearScreen) process.stdout.write("\x1Bc");
773
+ }
774
+ async #runTests(port, hmrPort, filters) {
775
+ await this.#hooks.runner("testsStarting").run(this);
776
+ debug_default("running tests using \"%s\" file, options %O", this.scriptFile, this.options);
777
+ return new Promise(async (resolve) => {
778
+ const mergedFilters = {
779
+ ...this.options.filters,
780
+ ...filters
781
+ };
782
+ const scriptArgs = [
783
+ ...this.#convertOptionsToArgs(),
784
+ ...this.options.scriptArgs,
785
+ ...this.#convertFiltersToArgs(mergedFilters)
786
+ ];
787
+ this.#testsProcess = runNode(this.cwd, {
788
+ script: this.scriptFile,
789
+ reject: true,
790
+ env: {
791
+ PORT: port,
792
+ VITE_HMR_PORT: hmrPort,
793
+ ...this.options.env
794
+ },
795
+ nodeArgs: this.options.nodeArgs,
796
+ scriptArgs
797
+ });
798
+ this.#testsProcess.then((result) => {
799
+ this.#hooks.runner("testsFinished").run(this).catch((error) => {
800
+ this.ui.logger.error("One of the \"testsFinished\" hooks failed");
801
+ this.ui.logger.fatal(error);
802
+ }).finally(() => {
803
+ if (!this.#watcher) {
804
+ this.#onClose?.(result.exitCode);
805
+ this.close();
806
+ }
807
+ });
808
+ }).catch((error) => {
809
+ if (!this.#watcher) {
810
+ this.#onError?.(error);
811
+ this.close();
812
+ } else this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
813
+ }).finally(() => resolve());
814
+ });
815
+ }
816
+ #handleFileChange(relativePath, absolutePath, action) {
817
+ const file = this.#fileSystem.inspect(absolutePath, relativePath);
818
+ if (!file) return;
819
+ this.#clearScreen();
820
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
821
+ if (file.fileType === "test") this.#reRunTests({ files: [relativePath] });
822
+ else this.#reRunTests();
823
+ }
824
+ #regenerateIndex(filePath, action) {
825
+ if (action === "add") return this.#indexGenerator.addFile(filePath);
826
+ return this.#indexGenerator.removeFile(filePath);
827
+ }
828
+ #registerServerRestartHooks() {
829
+ this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
830
+ this.#regenerateIndex(absolutePath, "add");
831
+ this.#handleFileChange(relativePath, absolutePath, "add");
832
+ });
833
+ this.#hooks.add("fileChanged", (relativePath, absolutePath) => {
834
+ this.#regenerateIndex(absolutePath, "add");
835
+ this.#handleFileChange(relativePath, absolutePath, "update");
836
+ });
837
+ this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
838
+ this.#regenerateIndex(absolutePath, "delete");
839
+ this.#handleFileChange(relativePath, absolutePath, "delete");
840
+ });
841
+ }
842
+ onClose(callback) {
843
+ this.#onClose = callback;
844
+ return this;
845
+ }
846
+ onError(callback) {
847
+ this.#onError = callback;
848
+ return this;
849
+ }
850
+ async close() {
851
+ await this.#watcher?.close();
852
+ if (this.#testsProcess) {
853
+ this.#testsProcess.removeAllListeners();
854
+ this.#testsProcess.kill("SIGKILL");
855
+ }
856
+ }
857
+ async run() {
858
+ this.#stickyPort = String(await getPort(this.cwd));
859
+ this.#stickyHmrPort = String(await getRandomPort({ port: 24678 }));
860
+ this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
861
+ this.#clearScreen();
862
+ this.ui.logger.info("loading hooks...");
863
+ this.#hooks = await loadHooks(this.options.hooks, [
864
+ "init",
865
+ "testsStarting",
866
+ "testsFinished"
867
+ ]);
868
+ await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
869
+ this.#hooks.clear("init");
870
+ this.ui.logger.info("generating indexes...");
871
+ await this.#indexGenerator.generate();
872
+ this.ui.logger.info("booting application to run tests...");
873
+ await this.#runTests(this.#stickyPort, this.#stickyHmrPort);
874
+ }
875
+ async runAndWatch(options) {
876
+ const tsConfig = readTsConfig(this.cwdPath);
877
+ if (!tsConfig) {
878
+ this.#onError?.(new RuntimeException("Unable to parse tsconfig file"));
879
+ return;
880
+ }
881
+ this.#stickyPort = String(await getPort(this.cwd));
882
+ this.#stickyHmrPort = String(getRandomPort({ port: 24678 }));
883
+ this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
884
+ this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, {
885
+ ...this.options,
886
+ suites: this.options.suites?.filter((suite) => {
887
+ if (this.options.filters.suites) return this.options.filters.suites.includes(suite.name);
888
+ return true;
889
+ })
890
+ });
891
+ this.#clearScreen();
892
+ this.ui.logger.info("loading hooks...");
893
+ this.#hooks = await loadHooks(this.options.hooks, [
894
+ "init",
895
+ "testsStarting",
896
+ "testsFinished",
897
+ "fileAdded",
898
+ "fileChanged",
899
+ "fileRemoved"
900
+ ]);
901
+ this.#registerServerRestartHooks();
902
+ await this.#hooks.runner("init").run(this, this.#hooks, this.#indexGenerator);
903
+ this.#hooks.clear("init");
904
+ this.ui.logger.info("generating indexes...");
905
+ await this.#indexGenerator.generate();
906
+ this.ui.logger.info("booting application to run tests...");
907
+ await this.#runTests(this.#stickyPort, this.#stickyHmrPort);
908
+ this.#watcher = watch({
909
+ usePolling: options?.poll ?? false,
910
+ cwd: this.cwdPath,
911
+ ignoreInitial: true,
912
+ ignored: (file, stats) => {
913
+ if (!stats) return false;
914
+ if (stats.isFile()) return !this.#fileSystem.shouldWatchFile(file);
915
+ return !this.#fileSystem.shouldWatchDirectory(file);
916
+ }
917
+ });
918
+ this.#watcher.on("ready", () => {
919
+ this.ui.logger.info("watching file system for changes...");
920
+ });
921
+ this.#watcher.on("error", (error) => {
922
+ this.ui.logger.warning("file system watcher failure");
923
+ this.ui.logger.fatal(error);
924
+ this.#onError?.(error);
925
+ this.#watcher?.close();
926
+ });
927
+ this.#watcher.on("add", (filePath) => {
928
+ const relativePath = string.toUnixSlash(filePath);
929
+ const absolutePath = join(this.cwdPath, filePath);
930
+ this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
931
+ });
932
+ this.#watcher.on("change", (filePath) => {
933
+ const relativePath = string.toUnixSlash(filePath);
934
+ const absolutePath = join(this.cwdPath, filePath);
935
+ this.#hooks.runner("fileChanged").run(relativePath, absolutePath, {
936
+ source: "watcher",
937
+ fullReload: true,
938
+ hotReloaded: false
939
+ }, this);
940
+ });
941
+ this.#watcher.on("unlink", (filePath) => {
942
+ const relativePath = string.toUnixSlash(filePath);
943
+ const absolutePath = join(this.cwdPath, filePath);
944
+ this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
945
+ });
946
+ }
1741
947
  };
948
+ export { Bundler, CodemodException, DevServer, FileBuffer, SUPPORTED_PACKAGE_MANAGERS, TestRunner, VirtualFileSystem };