@adonisjs/assembler 8.0.0-next.21 → 8.0.0-next.23

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-DM1KRNbk.js";
2
+ import { n as FileBuffer, t as IndexGenerator } from "./main-1eXSE5Xo.js";
3
+ import { t as RoutesScanner } from "./main-Byxt3AdL.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,1529 +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 change events to avoid repeated object creation
640
- */
641
- static #HOT_HOOK_CHANGE_INFO = {
642
- source: "hot-hook",
643
- fullReload: false,
644
- hotReloaded: false
645
- };
646
- /**
647
- * Pre-allocated info object for hot-hook full reload events
648
- */
649
- static #HOT_HOOK_FULL_RELOAD_INFO = {
650
- source: "hot-hook",
651
- fullReload: true,
652
- hotReloaded: false
653
- };
654
- /**
655
- * Pre-allocated info object for hot-hook invalidation events
656
- */
657
- static #HOT_HOOK_INVALIDATED_INFO = {
658
- source: "hot-hook",
659
- fullReload: false,
660
- hotReloaded: true
661
- };
662
- /**
663
- * Pre-allocated info object for file watcher events
664
- */
665
- static #WATCHER_INFO = {
666
- source: "watcher",
667
- fullReload: true,
668
- hotReloaded: false
669
- };
670
- /**
671
- * External listeners that are invoked when child process
672
- * gets an error or closes
673
- */
674
- #onError;
675
- #onClose;
676
- /**
677
- * The stickyPort is set by the start and the startAndWatch methods
678
- * and we will continue to use that port during restart
679
- */
680
- #stickyPort;
681
- /**
682
- * The mode is set by the start and the startAndWatch methods
683
- */
684
- #mode = "static";
685
- /**
686
- * Reference to chokidar watcher
687
- */
688
- #watcher;
689
- /**
690
- * Reference to the child process
691
- */
692
- #httpServer;
693
- /**
694
- * Keyboard shortcuts manager instance
695
- */
696
- #shortcutsManager;
697
- /**
698
- * Filesystem is used to decide which files to watch or entertain when
699
- * using hot-hook
700
- */
701
- #fileSystem;
702
- /**
703
- * Index generator for managing auto-generated index files
704
- */
705
- #indexGenerator;
706
- /**
707
- * Routes scanner to scan routes and infer route request and
708
- * response data
709
- */
710
- #routesScanner;
711
- /**
712
- * Hooks to execute custom actions during the dev server lifecycle
713
- */
714
- #hooks;
715
- /**
716
- * CLI UI instance for displaying colorful messages and progress information
717
- */
718
- ui = cliui2();
719
- /**
720
- * Restarts the HTTP server and throttle concurrent calls to
721
- * ensure we do not end up with a long loop of restarts
722
- */
723
- #restartHTTPServer = throttle(async () => {
724
- if (this.#httpServer) {
725
- this.#httpServer.removeAllListeners();
726
- this.#httpServer.kill("SIGKILL");
727
- }
728
- await this.#startHTTPServer(this.#stickyPort);
729
- }, "restartHTTPServer");
730
- /**
731
- * Sets up keyboard shortcuts for development server interactions
732
- *
733
- * Initializes the shortcuts manager with callbacks for restarting the server,
734
- * clearing the screen, and quitting the application.
735
- */
736
- #setupKeyboardShortcuts() {
737
- this.#shortcutsManager = new ShortcutsManager({
738
- logger: this.ui.logger,
739
- callbacks: {
740
- onRestart: () => this.#restartHTTPServer(),
741
- onClear: () => this.#clearScreen(),
742
- onQuit: () => this.close()
743
- }
744
- });
745
- this.#shortcutsManager.setup();
746
- }
747
- /**
748
- * Cleanup keyboard shortcuts and restore terminal state
749
- *
750
- * Removes keyboard shortcuts event listeners and restores the terminal
751
- * to its normal state when shutting down the development server.
752
- */
753
- #cleanupKeyboardShortcuts() {
754
- this.#shortcutsManager?.cleanup();
755
- }
756
- /**
757
- * The mode in which the DevServer is running
758
- *
759
- * Returns the current operating mode of the development server:
760
- * - 'hmr': Hot Module Reloading enabled
761
- * - 'watch': File system watching with full restarts
762
- * - 'static': No file watching or hot reloading
763
- */
764
- get mode() {
765
- return this.#mode;
766
- }
767
- /**
768
- * Script file to start the development server
769
- */
770
- scriptFile = "bin/server.ts";
771
- /**
772
- * The current working directory URL
773
- */
774
- cwd;
775
- /**
776
- * File path computed from the cwd
777
- */
778
- cwdPath;
779
- /**
780
- * Development server configuration options including hooks and environment variables
781
- */
782
- options;
783
- /**
784
- * Create a new DevServer instance
785
- *
786
- * @param cwd - The current working directory URL
787
- * @param options - Development server configuration options
788
- */
789
- constructor(cwd, options) {
790
- this.cwd = cwd;
791
- this.options = options;
792
- this.cwdPath = string3.toUnixSlash(fileURLToPath2(this.cwd));
793
- }
794
- /**
795
- * Type guard to check if child process message is from AdonisJS HTTP server
796
- *
797
- * Validates that a message from the child process contains the expected
798
- * structure indicating the AdonisJS server is ready and listening.
799
- *
800
- * @param message - Unknown message from child process
801
- * @returns True if message is an AdonisJS ready message
802
- */
803
- #isAdonisJSReadyMessage(message) {
804
- return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
805
- }
806
- /**
807
- * Type guard to check if child process message contains routes information
808
- *
809
- * Validates that a message from the child process contains the expected
810
- * structure with routes file location from the AdonisJS server.
811
- *
812
- * @param message - Unknown message from child process
813
- * @returns True if message contains routes file location
814
- */
815
- #isAdonisJSRoutesMessage(message) {
816
- return message !== null && typeof message === "object" && "routesFileLocation" in message;
817
- }
818
- /**
819
- * Displays server information and executes hooks after server startup
820
- *
821
- * Shows server URL, mode, startup duration, and help instructions.
822
- * Also executes the devServerStarted hooks to allow custom post-startup logic.
823
- *
824
- * @param message - Server ready message containing port, host, and optional duration
825
- */
826
- async #postServerReady(message) {
827
- const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
828
- const info = { host, port: message.port };
829
- const serverUrl = `http://${host}:${message.port}`;
830
- this.#shortcutsManager?.setServerUrl(serverUrl);
831
- const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(serverUrl)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
832
- if (message.duration) {
833
- displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
834
- }
835
- displayMessage.add(`Press ${this.ui.colors.dim("h")} to show help`);
836
- try {
837
- await this.#hooks.runner("devServerStarted").run(this, info, displayMessage);
838
- } catch (error) {
839
- this.ui.logger.error('One of the "devServerStarted" hooks failed');
840
- this.ui.logger.fatal(error);
841
- }
842
- displayMessage.render();
843
- }
844
- /**
845
- * Type guard to check if child process message is from hot-hook
846
- *
847
- * Validates that a message from the child process is a hot-hook notification
848
- * about file changes, invalidations, or full reloads.
849
- *
850
- * @param message - Unknown message from child process
851
- * @returns True if message is a hot-hook message
852
- */
853
- #isHotHookMessage(message) {
854
- return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
855
- }
856
- /**
857
- * Conditionally clears the terminal screen based on configuration
858
- *
859
- * Clears the terminal screen if the clearScreen option is enabled,
860
- * providing a clean view for development output.
861
- */
862
- #clearScreen() {
863
- if (this.options.clearScreen) {
864
- process.stdout.write("\x1Bc");
865
- }
866
- }
867
- /**
868
- * Handles file change events and triggers appropriate server actions
869
- *
870
- * Processes file change notifications and determines whether to restart
871
- * the server, hot reload, or ignore the change based on file type and mode.
872
- *
873
- * @param relativePath - Relative path to the changed file
874
- * @param absolutePath - Absolute path to the changed file
875
- * @param action - Type of file change (add, update, delete)
876
- * @param info - Optional information about the change source and reload behavior
877
- */
878
- #handleFileChange(relativePath, absolutePath, action, info) {
879
- if ((action === "add" || action === "delete") && this.mode === "hmr") {
880
- debug_default("ignoring add and delete actions in HMR mode %s", relativePath);
881
- return;
882
- }
883
- if (info && info.source === "hot-hook" && info.hotReloaded) {
884
- debug_default("hot reloading %s, info %O", relativePath, info);
885
- this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${relativePath}`);
886
- return;
887
- }
888
- if (info && !info.fullReload) {
889
- debug_default("ignoring full reload", relativePath, info);
890
- return;
891
- }
892
- const file = this.#fileSystem.inspect(absolutePath, relativePath);
893
- if (!file) {
894
- return;
895
- }
896
- if (file.reloadServer) {
897
- this.#clearScreen();
898
- this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
899
- this.#restartHTTPServer();
900
- return;
901
- }
902
- this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
903
- }
904
- /**
905
- * Regenerates index files when a file is added or removed
906
- *
907
- * Updates the index generator to reflect file system changes by adding
908
- * or removing files from the generated index files.
909
- *
910
- * @param filePath - Absolute path to the file that changed
911
- * @param action - Whether the file was added or deleted
912
- */
913
- #regenerateIndex(filePath, action) {
914
- if (action === "add") {
915
- return this.#indexGenerator.addFile(filePath);
916
- }
917
- return this.#indexGenerator.removeFile(filePath);
918
- }
919
- /**
920
- * Re-scans routes when a file is modified during hot reloading
921
- *
922
- * Invalidates the routes cache for the given file and triggers route
923
- * scanning hooks if the invalidation was successful.
924
- *
925
- * @param filePath - Absolute path to the file that was modified
926
- *
927
- * @example
928
- * await devServer.#reScanRoutes('/path/to/routes.ts')
929
- */
930
- async #reScanRoutes(filePath) {
931
- if (!this.#routesScanner) {
932
- return;
933
- }
934
- try {
935
- const invalidated = await this.#routesScanner.invalidate(filePath);
936
- if (invalidated) {
937
- await this.#hooks.runner("routesScanned").run(this, this.#routesScanner);
938
- }
939
- } catch (error) {
940
- this.ui.logger.error("Unable to rescan routes because of the following error");
941
- this.ui.logger.fatal(error);
942
- }
943
- }
944
- /**
945
- * Processes routes received from the AdonisJS server
946
- *
947
- * Executes routesCommitted hooks and optionally scans routes if scanning
948
- * hooks are registered. Creates a routes scanner instance if needed and
949
- * processes routes for each domain.
950
- *
951
- * @param routesList - Routes organized by domain
952
- *
953
- * @example
954
- * await devServer.#processRoutes({
955
- * 'example.com': [
956
- * { pattern: '/', handler: 'HomeController.index' }
957
- * ]
958
- * })
959
- */
960
- #processRoutes = throttle(async (routesFileLocation) => {
961
- try {
962
- const scanRoutes = this.#hooks.has("routesScanning") || this.#hooks.has("routesScanned");
963
- const shareRoutes = this.#hooks.has("routesCommitted");
964
- if (!scanRoutes && !shareRoutes) {
965
- unlink(routesFileLocation).catch(() => {
966
- });
967
- return;
968
- }
969
- const routesJSON = await readFile(routesFileLocation, "utf-8");
970
- const routesList = JSON.parse(routesJSON);
971
- unlink(routesFileLocation).catch(() => {
972
- });
973
- if (shareRoutes) {
974
- await this.#hooks.runner("routesCommitted").run(this, routesList);
975
- }
976
- if (scanRoutes) {
977
- this.#routesScanner = new RoutesScanner(this.cwdPath, []);
978
- await this.#hooks.runner("routesScanning").run(this, this.#routesScanner);
979
- for (const domain of Object.keys(routesList)) {
980
- await this.#routesScanner.scan(routesList[domain]);
981
- }
982
- await this.#hooks.runner("routesScanned").run(this, this.#routesScanner);
983
- }
984
- } catch (error) {
985
- this.ui.logger.error("Unable to process and scan routes because of the following error");
986
- this.ui.logger.fatal(error);
987
- }
988
- }, "processRoutes");
989
- /**
990
- * Registers hooks for file system events and server restart triggers
991
- *
992
- * Sets up event handlers that respond to file additions, changes, and removals
993
- * by regenerating indexes and handling server restarts as needed.
994
- */
995
- #registerServerRestartHooks() {
996
- this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
997
- this.#regenerateIndex(absolutePath, "add");
998
- this.#handleFileChange(relativePath, absolutePath, "add");
999
- });
1000
- this.#hooks.add("fileChanged", (relativePath, absolutePath, info) => {
1001
- if (info.hotReloaded || !info.hotReloaded && !info.fullReload) {
1002
- this.#reScanRoutes(absolutePath);
1003
- }
1004
- this.#handleFileChange(relativePath, absolutePath, "update", info);
1005
- });
1006
- this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
1007
- this.#regenerateIndex(absolutePath, "delete");
1008
- this.#handleFileChange(relativePath, absolutePath, "delete");
1009
- });
1010
- }
1011
- /**
1012
- * Initializes the development server state and executes init hooks
1013
- *
1014
- * Parses TypeScript configuration, sets up file system, loads hooks,
1015
- * initializes the index generator, and prepares the server for the
1016
- * specified mode (HMR, watch, or static).
1017
- *
1018
- * @param ts - TypeScript module reference
1019
- * @param mode - Server mode (hmr, watch, or static)
1020
- * @returns True if initialization succeeds, false if tsconfig parsing fails
1021
- *
1022
- * @example
1023
- * const success = await devServer.#init(ts, 'hmr')
1024
- * if (!success) {
1025
- * console.error('Failed to initialize dev server')
1026
- * }
1027
- */
1028
- async #init(mode) {
1029
- const tsConfig = readTsConfig(this.cwdPath);
1030
- if (!tsConfig) {
1031
- this.#onError?.(new RuntimeException("Unable to parse tsconfig file"));
1032
- return false;
1033
- }
1034
- this.#mode = mode;
1035
- this.#clearScreen();
1036
- this.ui.logger.info(`starting server in ${this.#mode} mode...`);
1037
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1038
- this.#stickyPort = String(await getPort(this.cwd));
1039
- this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, this.options);
1040
- this.ui.logger.info("loading hooks...");
1041
- this.#hooks = await loadHooks(this.options.hooks, [
1042
- "init",
1043
- "routesCommitted",
1044
- "routesScanning",
1045
- "routesScanned",
1046
- "devServerStarting",
1047
- "devServerStarted",
1048
- "fileAdded",
1049
- "fileChanged",
1050
- "fileRemoved"
1051
- ]);
1052
- this.#registerServerRestartHooks();
1053
- this.#setupKeyboardShortcuts();
1054
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
1055
- this.#hooks.clear("init");
1056
- this.ui.logger.info("generating indexes...");
1057
- await this.#indexGenerator.generate();
1058
- return true;
1059
- }
1060
- /**
1061
- * Starts the HTTP server as a child process
1062
- *
1063
- * Creates a new Node.js child process to run the server script with the
1064
- * specified port and configuration. Sets up message handlers for server
1065
- * ready notifications, routes sharing, and hot-hook events. Executes
1066
- * devServerStarting hooks before spawning the process.
1067
- *
1068
- * @param port - Port number for the server to listen on
1069
- *
1070
- * @example
1071
- * await devServer.#startHTTPServer('3333')
1072
- */
1073
- async #startHTTPServer(port) {
1074
- await this.#hooks.runner("devServerStarting").run(this);
1075
- debug_default('starting http server using "%s" file, options %O', this.scriptFile, this.options);
1076
- return new Promise(async (resolve) => {
1077
- this.#httpServer = runNode(this.cwd, {
1078
- script: this.scriptFile,
1079
- env: { PORT: port, ...this.options.env },
1080
- nodeArgs: this.options.nodeArgs,
1081
- reject: true,
1082
- scriptArgs: this.options.scriptArgs
1083
- });
1084
- this.#httpServer.on("message", async (message) => {
1085
- if (this.#isAdonisJSReadyMessage(message)) {
1086
- debug_default("received http server ready message %O", message);
1087
- await this.#postServerReady(message);
1088
- resolve();
1089
- } else if (this.#isAdonisJSRoutesMessage(message)) {
1090
- debug_default("received routes location from the server %O", message);
1091
- await this.#processRoutes(message.routesFileLocation);
1092
- } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
1093
- debug_default("received hot-hook message %O", message);
1094
- if (message.type === "hot-hook:file-changed") {
1095
- const absolutePath = message.path ? string3.toUnixSlash(message.path) : "";
1096
- const relativePath = relative3(this.cwdPath, absolutePath);
1097
- if (message.action === "add") {
1098
- this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1099
- } else if (message.action === "change") {
1100
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_CHANGE_INFO, this);
1101
- } else if (message.action === "unlink") {
1102
- this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1103
- }
1104
- } else if (message.type === "hot-hook:full-reload") {
1105
- const absolutePath = message.path ? string3.toUnixSlash(message.path) : "";
1106
- const relativePath = relative3(this.cwdPath, absolutePath);
1107
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_FULL_RELOAD_INFO, this);
1108
- } else if (message.type === "hot-hook:invalidated") {
1109
- const absolutePath = message.paths[0] ? string3.toUnixSlash(message.paths[0]) : "";
1110
- const relativePath = relative3(this.cwdPath, absolutePath);
1111
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#HOT_HOOK_INVALIDATED_INFO, this);
1112
- }
1113
- }
1114
- });
1115
- this.#httpServer.then((result) => {
1116
- if (!this.#watcher) {
1117
- this.#onClose?.(result.exitCode);
1118
- } else {
1119
- this.ui.logger.info("Underlying HTTP server closed. Still watching for changes");
1120
- }
1121
- }).catch((error) => {
1122
- if (!this.#watcher) {
1123
- this.#onError?.(error);
1124
- } else {
1125
- this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
1126
- }
1127
- }).finally(() => {
1128
- resolve();
1129
- });
1130
- });
1131
- }
1132
- /**
1133
- * Adds listener to get notified when dev server is closed
1134
- *
1135
- * Registers a callback function that will be invoked when the development
1136
- * server's child process exits. The callback receives the exit code.
1137
- *
1138
- * @param callback - Function to call when dev server closes
1139
- * @returns This DevServer instance for method chaining
1140
- *
1141
- * @example
1142
- * devServer.onClose((exitCode) => {
1143
- * console.log(`Server closed with exit code: ${exitCode}`)
1144
- * })
1145
- */
1146
- onClose(callback) {
1147
- this.#onClose = callback;
1148
- return this;
1149
- }
1150
- /**
1151
- * Adds listener to get notified when dev server encounters an error
1152
- *
1153
- * Registers a callback function that will be invoked when the development
1154
- * server's child process encounters an error or fails to start.
1155
- *
1156
- * @param callback - Function to call when dev server encounters an error
1157
- * @returns This DevServer instance for method chaining
1158
- *
1159
- * @example
1160
- * devServer.onError((error) => {
1161
- * console.error('Dev server error:', error.message)
1162
- * })
1163
- */
1164
- onError(callback) {
1165
- this.#onError = callback;
1166
- return this;
1167
- }
1168
- /**
1169
- * Closes watchers and terminates the running child process
1170
- *
1171
- * Cleans up keyboard shortcuts, stops file system watchers, and kills
1172
- * the HTTP server child process. This should be called when shutting down
1173
- * the development server.
1174
- *
1175
- * @example
1176
- * await devServer.close()
1177
- */
1178
- async close() {
1179
- this.#cleanupKeyboardShortcuts();
1180
- await this.#watcher?.close();
1181
- if (this.#httpServer) {
1182
- this.#httpServer.removeAllListeners();
1183
- this.#httpServer.kill("SIGKILL");
1184
- }
1185
- }
1186
- /**
1187
- * Starts the development server in static or HMR mode
1188
- *
1189
- * Initializes the server and starts the HTTP server. The mode is determined
1190
- * by the `hmr` option in DevServerOptions. In HMR mode, hot-hook is configured
1191
- * to enable hot module reloading.
1192
- *
1193
- * @param ts - TypeScript module reference
1194
- *
1195
- * @example
1196
- * const devServer = new DevServer(cwd, { hmr: true, hooks: [] })
1197
- * await devServer.start(ts)
1198
- */
1199
- async start() {
1200
- const initiated = await this.#init(this.options.hmr ? "hmr" : "static");
1201
- if (!initiated) {
1202
- return;
1203
- }
1204
- if (this.#mode === "hmr") {
1205
- this.options.nodeArgs.push("--import=hot-hook/register");
1206
- this.options.env = {
1207
- ...this.options.env,
1208
- HOT_HOOK_INCLUDE: this.#fileSystem.includes.join(","),
1209
- HOT_HOOK_IGNORE: this.#fileSystem.excludes.filter((exclude) => !exclude.includes("inertia")).join(","),
1210
- HOT_HOOK_RESTART: (this.options.metaFiles ?? []).filter(({ reloadServer }) => !!reloadServer).map(({ pattern }) => pattern).join(",")
1211
- };
1212
- }
1213
- this.ui.logger.info("starting HTTP server...");
1214
- await this.#startHTTPServer(this.#stickyPort);
1215
- }
1216
- /**
1217
- * Starts the development server in watch mode and restarts on file changes
1218
- *
1219
- * Initializes the server, starts the HTTP server, and sets up a file system
1220
- * watcher that monitors for changes. When files are added, modified, or deleted,
1221
- * the server automatically restarts. The watcher respects TypeScript project
1222
- * configuration and metaFiles settings.
1223
- *
1224
- * @param ts - TypeScript module reference
1225
- * @param options - Watch options including polling mode
1226
- *
1227
- * @example
1228
- * const devServer = new DevServer(cwd, { hooks: [] })
1229
- * await devServer.startAndWatch(ts, { poll: false })
1230
- */
1231
- async startAndWatch(options) {
1232
- const initiated = await this.#init("watch");
1233
- if (!initiated) {
1234
- return;
1235
- }
1236
- this.ui.logger.info("starting HTTP server...");
1237
- await this.#startHTTPServer(this.#stickyPort);
1238
- this.#watcher = watch({
1239
- usePolling: options?.poll ?? false,
1240
- cwd: this.cwdPath,
1241
- ignoreInitial: true,
1242
- ignored: (file, stats) => {
1243
- if (!stats) {
1244
- return false;
1245
- }
1246
- if (file.includes("inertia") && !file.includes("node_modules")) {
1247
- return false;
1248
- }
1249
- if (stats.isFile()) {
1250
- return !this.#fileSystem.shouldWatchFile(file);
1251
- }
1252
- return !this.#fileSystem.shouldWatchDirectory(file);
1253
- }
1254
- });
1255
- this.#watcher.on("ready", () => {
1256
- this.ui.logger.info("watching file system for changes...");
1257
- });
1258
- this.#watcher.on("error", (error) => {
1259
- this.ui.logger.warning("file system watcher failure");
1260
- this.ui.logger.fatal(error);
1261
- this.#onError?.(error);
1262
- this.#watcher?.close();
1263
- });
1264
- this.#watcher.on("add", (filePath) => {
1265
- const relativePath = string3.toUnixSlash(filePath);
1266
- const absolutePath = join2(this.cwdPath, relativePath);
1267
- this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1268
- });
1269
- this.#watcher.on("change", (filePath) => {
1270
- const relativePath = string3.toUnixSlash(filePath);
1271
- const absolutePath = join2(this.cwdPath, relativePath);
1272
- this.#hooks.runner("fileChanged").run(relativePath, absolutePath, _DevServer.#WATCHER_INFO, this);
1273
- });
1274
- this.#watcher.on("unlink", (filePath) => {
1275
- const relativePath = string3.toUnixSlash(filePath);
1276
- const absolutePath = join2(this.cwdPath, relativePath);
1277
- this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1278
- });
1279
- }
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
+ }
1280
698
  };
1281
-
1282
- // src/test_runner.ts
1283
- import { join as join3 } from "path/posix";
1284
- import { cliui as cliui3 } from "@poppinss/cliui";
1285
- import { fileURLToPath as fileURLToPath3 } from "url";
1286
- import string4 from "@poppinss/utils/string";
1287
- import { RuntimeException as RuntimeException2 } from "@poppinss/utils/exception";
1288
699
  var TestRunner = class {
1289
- /**
1290
- * External listeners that are invoked when child process
1291
- * gets an error or closes
1292
- */
1293
- #onError;
1294
- #onClose;
1295
- /**
1296
- * The stickyPort is set by the startAndWatch method and we will
1297
- * continue to use this port during re-runs
1298
- */
1299
- #stickyPort;
1300
- /**
1301
- * Reference to chokidar watcher
1302
- */
1303
- #watcher;
1304
- /**
1305
- * Reference to the test script child process
1306
- */
1307
- #testsProcess;
1308
- /**
1309
- * Filesystem is used to decide which files to watch or entertain in watch
1310
- * mode
1311
- */
1312
- #fileSystem;
1313
- /**
1314
- * Hooks to execute custom actions during the tests runner lifecycle
1315
- */
1316
- #hooks;
1317
- /**
1318
- * Index generator for managing auto-generated index files
1319
- */
1320
- #indexGenerator;
1321
- /**
1322
- * CLI UI instance for displaying colorful messages and progress information
1323
- */
1324
- ui = cliui3();
1325
- /**
1326
- * Re-runs the test child process and throttle concurrent calls to
1327
- * ensure we do not end up with a long loop of restarts
1328
- */
1329
- #reRunTests = throttle(async (filters) => {
1330
- if (this.#testsProcess) {
1331
- this.#testsProcess.removeAllListeners();
1332
- this.#testsProcess.kill("SIGKILL");
1333
- }
1334
- await this.#runTests(this.#stickyPort, filters);
1335
- }, "reRunTests");
1336
- /**
1337
- * The script file to run as a child process
1338
- */
1339
- scriptFile = "bin/test.ts";
1340
- /**
1341
- * The current working directory URL
1342
- */
1343
- cwd;
1344
- /**
1345
- * The current working directory path as a string
1346
- */
1347
- cwdPath;
1348
- /**
1349
- * Test runner configuration options including filters, reporters, and hooks
1350
- */
1351
- options;
1352
- /**
1353
- * Create a new TestRunner instance
1354
- *
1355
- * @param cwd - The current working directory URL
1356
- * @param options - Test runner configuration options
1357
- */
1358
- constructor(cwd, options) {
1359
- this.cwd = cwd;
1360
- this.options = options;
1361
- this.cwdPath = string4.toUnixSlash(fileURLToPath3(this.cwd));
1362
- }
1363
- /**
1364
- * Convert test runner options to the CLI args
1365
- *
1366
- * Transforms the test runner configuration options into command-line
1367
- * arguments that can be passed to the test script.
1368
- *
1369
- * @returns Array of command-line arguments
1370
- */
1371
- #convertOptionsToArgs() {
1372
- const args = [];
1373
- if (this.options.reporters) {
1374
- args.push("--reporters");
1375
- args.push(this.options.reporters.join(","));
1376
- }
1377
- if (this.options.timeout !== void 0) {
1378
- args.push("--timeout");
1379
- args.push(String(this.options.timeout));
1380
- }
1381
- if (this.options.failed) {
1382
- args.push("--failed");
1383
- }
1384
- if (this.options.retries !== void 0) {
1385
- args.push("--retries");
1386
- args.push(String(this.options.retries));
1387
- }
1388
- return args;
1389
- }
1390
- /**
1391
- * Converts all known filters to CLI args
1392
- *
1393
- * Transforms test filters (suites, files, groups, tags, tests) into
1394
- * command-line arguments for the test script.
1395
- *
1396
- * @param filters - The test filters to convert
1397
- * @returns Array of command-line arguments representing the filters
1398
- */
1399
- #convertFiltersToArgs(filters) {
1400
- const args = [];
1401
- if (filters.suites) {
1402
- args.push(...filters.suites);
1403
- }
1404
- if (filters.files) {
1405
- args.push("--files");
1406
- args.push(filters.files.join(","));
1407
- }
1408
- if (filters.groups) {
1409
- args.push("--groups");
1410
- args.push(filters.groups.join(","));
1411
- }
1412
- if (filters.tags) {
1413
- args.push("--tags");
1414
- args.push(filters.tags.join(","));
1415
- }
1416
- if (filters.tests) {
1417
- args.push("--tests");
1418
- args.push(filters.tests.join(","));
1419
- }
1420
- return args;
1421
- }
1422
- /**
1423
- * Conditionally clear the terminal screen
1424
- *
1425
- * Clears the terminal screen if the clearScreen option is enabled
1426
- * in the test runner configuration.
1427
- */
1428
- #clearScreen() {
1429
- if (this.options.clearScreen) {
1430
- process.stdout.write("\x1Bc");
1431
- }
1432
- }
1433
- /**
1434
- * Runs tests as a child process
1435
- *
1436
- * Creates a Node.js child process to execute the test script with
1437
- * appropriate command-line arguments and environment variables.
1438
- * Handles process lifecycle and hook execution.
1439
- *
1440
- * @param port - The port number to set in the environment
1441
- * @param filters - Optional test filters to apply for this run
1442
- */
1443
- async #runTests(port, filters) {
1444
- await this.#hooks.runner("testsStarting").run(this);
1445
- debug_default('running tests using "%s" file, options %O', this.scriptFile, this.options);
1446
- return new Promise(async (resolve) => {
1447
- const mergedFilters = { ...this.options.filters, ...filters };
1448
- const scriptArgs = [
1449
- ...this.#convertOptionsToArgs(),
1450
- ...this.options.scriptArgs,
1451
- ...this.#convertFiltersToArgs(mergedFilters)
1452
- ];
1453
- this.#testsProcess = runNode(this.cwd, {
1454
- script: this.scriptFile,
1455
- reject: true,
1456
- env: { PORT: port, ...this.options.env },
1457
- nodeArgs: this.options.nodeArgs,
1458
- scriptArgs
1459
- });
1460
- this.#testsProcess.then((result) => {
1461
- this.#hooks.runner("testsFinished").run(this).catch((error) => {
1462
- this.ui.logger.error('One of the "testsFinished" hooks failed');
1463
- this.ui.logger.fatal(error);
1464
- }).finally(() => {
1465
- if (!this.#watcher) {
1466
- this.#onClose?.(result.exitCode);
1467
- this.close();
1468
- }
1469
- });
1470
- }).catch((error) => {
1471
- if (!this.#watcher) {
1472
- this.#onError?.(error);
1473
- this.close();
1474
- } else {
1475
- this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
1476
- }
1477
- }).finally(() => resolve());
1478
- });
1479
- }
1480
- /**
1481
- * Handles file change event during watch mode
1482
- *
1483
- * Determines whether to run specific tests or all tests based on
1484
- * the type of file that changed. Test files trigger selective runs,
1485
- * while other files trigger full test suite runs.
1486
- *
1487
- * @param filePath - The path of the changed file
1488
- * @param action - The type of change (add, update, delete)
1489
- */
1490
- #handleFileChange(relativePath, absolutePath, action) {
1491
- const file = this.#fileSystem.inspect(absolutePath, relativePath);
1492
- if (!file) {
1493
- return;
1494
- }
1495
- this.#clearScreen();
1496
- this.ui.logger.log(`${this.ui.colors.green(action)} ${relativePath}`);
1497
- if (file.fileType === "test") {
1498
- this.#reRunTests({ files: [relativePath] });
1499
- } else {
1500
- this.#reRunTests();
1501
- }
1502
- }
1503
- /**
1504
- * Re-generates the index when a file is changed, but only in HMR
1505
- * mode
1506
- */
1507
- #regenerateIndex(filePath, action) {
1508
- if (action === "add") {
1509
- return this.#indexGenerator.addFile(filePath);
1510
- }
1511
- return this.#indexGenerator.removeFile(filePath);
1512
- }
1513
- /**
1514
- * Registers inline hooks for file changes and test re-runs
1515
- *
1516
- * Sets up event handlers that respond to file system changes by
1517
- * triggering appropriate test runs based on the changed files.
1518
- */
1519
- #registerServerRestartHooks() {
1520
- this.#hooks.add("fileAdded", (relativePath, absolutePath) => {
1521
- this.#regenerateIndex(absolutePath, "add");
1522
- this.#handleFileChange(relativePath, absolutePath, "add");
1523
- });
1524
- this.#hooks.add("fileChanged", (relativePath, absolutePath) => {
1525
- this.#regenerateIndex(absolutePath, "add");
1526
- this.#handleFileChange(relativePath, absolutePath, "update");
1527
- });
1528
- this.#hooks.add("fileRemoved", (relativePath, absolutePath) => {
1529
- this.#regenerateIndex(absolutePath, "delete");
1530
- this.#handleFileChange(relativePath, absolutePath, "delete");
1531
- });
1532
- }
1533
- /**
1534
- * Add listener to get notified when test runner is closed
1535
- *
1536
- * @param callback - Function to call when test runner closes
1537
- * @returns This TestRunner instance for method chaining
1538
- */
1539
- onClose(callback) {
1540
- this.#onClose = callback;
1541
- return this;
1542
- }
1543
- /**
1544
- * Add listener to get notified when test runner encounters an error
1545
- *
1546
- * @param callback - Function to call when test runner encounters an error
1547
- * @returns This TestRunner instance for method chaining
1548
- */
1549
- onError(callback) {
1550
- this.#onError = callback;
1551
- return this;
1552
- }
1553
- /**
1554
- * Close watchers and running child processes
1555
- *
1556
- * Cleans up file system watchers and terminates any running test
1557
- * processes to ensure graceful shutdown.
1558
- */
1559
- async close() {
1560
- await this.#watcher?.close();
1561
- if (this.#testsProcess) {
1562
- this.#testsProcess.removeAllListeners();
1563
- this.#testsProcess.kill("SIGKILL");
1564
- }
1565
- }
1566
- /**
1567
- * Runs tests once without watching for file changes
1568
- *
1569
- * Executes the test suite a single time and exits. This is the
1570
- * equivalent of running tests in CI/CD environments.
1571
- */
1572
- async run() {
1573
- this.#stickyPort = String(await getPort(this.cwd));
1574
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1575
- this.#clearScreen();
1576
- this.ui.logger.info("loading hooks...");
1577
- this.#hooks = await loadHooks(this.options.hooks, ["init", "testsStarting", "testsFinished"]);
1578
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
1579
- this.#hooks.clear("init");
1580
- this.ui.logger.info("generating indexes...");
1581
- await this.#indexGenerator.generate();
1582
- this.ui.logger.info("booting application to run tests...");
1583
- await this.#runTests(this.#stickyPort);
1584
- }
1585
- /**
1586
- * Run tests in watch mode and re-run them when files change
1587
- *
1588
- * Starts the test runner in watch mode, monitoring the file system
1589
- * for changes and automatically re-running tests when relevant files
1590
- * are modified. Uses intelligent filtering to run only affected tests
1591
- * when possible.
1592
- *
1593
- * @param ts - TypeScript module reference for parsing configuration
1594
- * @param options - Watch options including polling mode for file system monitoring
1595
- */
1596
- async runAndWatch(options) {
1597
- const tsConfig = readTsConfig(this.cwdPath);
1598
- if (!tsConfig) {
1599
- this.#onError?.(new RuntimeException2("Unable to parse tsconfig file"));
1600
- return;
1601
- }
1602
- this.#stickyPort = String(await getPort(this.cwd));
1603
- this.#indexGenerator = new IndexGenerator(this.cwdPath, this.ui.logger);
1604
- this.#fileSystem = new FileSystem(this.cwdPath, tsConfig, {
1605
- ...this.options,
1606
- suites: this.options.suites?.filter((suite) => {
1607
- if (this.options.filters.suites) {
1608
- return this.options.filters.suites.includes(suite.name);
1609
- }
1610
- return true;
1611
- })
1612
- });
1613
- this.#clearScreen();
1614
- this.ui.logger.info("loading hooks...");
1615
- this.#hooks = await loadHooks(this.options.hooks, [
1616
- "init",
1617
- "testsStarting",
1618
- "testsFinished",
1619
- "fileAdded",
1620
- "fileChanged",
1621
- "fileRemoved"
1622
- ]);
1623
- this.#registerServerRestartHooks();
1624
- await this.#hooks.runner("init").run(this, this.#indexGenerator);
1625
- this.#hooks.clear("init");
1626
- this.ui.logger.info("generating indexes...");
1627
- await this.#indexGenerator.generate();
1628
- this.ui.logger.info("booting application to run tests...");
1629
- await this.#runTests(this.#stickyPort);
1630
- this.#watcher = watch({
1631
- usePolling: options?.poll ?? false,
1632
- cwd: this.cwdPath,
1633
- ignoreInitial: true,
1634
- ignored: (file, stats) => {
1635
- if (!stats) {
1636
- return false;
1637
- }
1638
- if (stats.isFile()) {
1639
- return !this.#fileSystem.shouldWatchFile(file);
1640
- }
1641
- return !this.#fileSystem.shouldWatchDirectory(file);
1642
- }
1643
- });
1644
- this.#watcher.on("ready", () => {
1645
- this.ui.logger.info("watching file system for changes...");
1646
- });
1647
- this.#watcher.on("error", (error) => {
1648
- this.ui.logger.warning("file system watcher failure");
1649
- this.ui.logger.fatal(error);
1650
- this.#onError?.(error);
1651
- this.#watcher?.close();
1652
- });
1653
- this.#watcher.on("add", (filePath) => {
1654
- const relativePath = string4.toUnixSlash(filePath);
1655
- const absolutePath = join3(this.cwdPath, filePath);
1656
- this.#hooks.runner("fileAdded").run(relativePath, absolutePath, this);
1657
- });
1658
- this.#watcher.on("change", (filePath) => {
1659
- const relativePath = string4.toUnixSlash(filePath);
1660
- const absolutePath = join3(this.cwdPath, filePath);
1661
- this.#hooks.runner("fileChanged").run(
1662
- relativePath,
1663
- absolutePath,
1664
- {
1665
- source: "watcher",
1666
- fullReload: true,
1667
- hotReloaded: false
1668
- },
1669
- this
1670
- );
1671
- });
1672
- this.#watcher.on("unlink", (filePath) => {
1673
- const relativePath = string4.toUnixSlash(filePath);
1674
- const absolutePath = join3(this.cwdPath, filePath);
1675
- this.#hooks.runner("fileRemoved").run(relativePath, absolutePath, this);
1676
- });
1677
- }
1678
- };
1679
- export {
1680
- Bundler,
1681
- DevServer,
1682
- FileBuffer,
1683
- SUPPORTED_PACKAGE_MANAGERS,
1684
- TestRunner,
1685
- 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
+ }
1686
936
  };
937
+ export { Bundler, DevServer, FileBuffer, SUPPORTED_PACKAGE_MANAGERS, TestRunner, VirtualFileSystem };