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