@adonisjs/assembler 8.0.0-next.22 → 8.0.0-next.24

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