@adonisjs/assembler 8.0.0-next.9 → 8.0.1

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