@adonisjs/assembler 7.8.2 → 8.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,124 +1,57 @@
1
+ import {
2
+ debug_default
3
+ } from "./chunk-RR4HCA4M.js";
4
+
1
5
  // src/bundler.ts
2
- import slash from "slash";
3
6
  import dedent from "dedent";
4
- import fs from "node:fs/promises";
5
- import { fileURLToPath as fileURLToPath2 } from "node:url";
6
- import { join as join2, relative as relative2 } from "node:path";
7
+ import fs from "fs/promises";
7
8
  import { cliui } from "@poppinss/cliui";
9
+ import { fileURLToPath as fileURLToPath2 } from "url";
10
+ import { join as join2, relative as relative2 } from "path";
11
+ import string from "@poppinss/utils/string";
8
12
  import { detectPackageManager } from "@antfu/install-pkg";
9
13
 
10
- // src/hooks.ts
11
- import { RuntimeException } from "@poppinss/utils";
12
- import Hooks from "@poppinss/hooks";
13
- var AssemblerHooks = class {
14
- #config;
15
- #hooks = new Hooks();
16
- constructor(config) {
17
- this.#config = config;
18
- }
19
- /**
20
- * Resolve the hook by importing the file and returning the default export
21
- */
22
- async #resolveHookNode(node) {
23
- const exports = await node();
24
- if (!exports.default) {
25
- throw new RuntimeException("Assembler hook must be defined using the default export");
26
- }
27
- return exports.default;
28
- }
29
- /**
30
- * Resolve hooks needed for dev-time and register them to the Hooks instance
31
- */
32
- async registerDevServerHooks() {
33
- await Promise.all([
34
- ...(this.#config?.onDevServerStarted || []).map(
35
- async (node) => this.#hooks.add("onDevServerStarted", await this.#resolveHookNode(node))
36
- ),
37
- ...(this.#config?.onSourceFileChanged || []).map(
38
- async (node) => this.#hooks.add("onSourceFileChanged", await this.#resolveHookNode(node))
39
- )
40
- ]);
41
- }
42
- /**
43
- * Resolve hooks needed for build-time and register them to the Hooks instance
44
- */
45
- async registerBuildHooks() {
46
- await Promise.all([
47
- ...(this.#config?.onBuildStarting || []).map(
48
- async (node) => this.#hooks.add("onBuildStarting", await this.#resolveHookNode(node))
49
- ),
50
- ...(this.#config?.onBuildCompleted || []).map(
51
- async (node) => this.#hooks.add("onBuildCompleted", await this.#resolveHookNode(node))
52
- )
53
- ]);
54
- }
55
- /**
56
- * When the dev server is started
57
- */
58
- async onDevServerStarted(...args) {
59
- await this.#hooks.runner("onDevServerStarted").run(...args);
60
- }
61
- /**
62
- * When a source file changes
63
- */
64
- async onSourceFileChanged(...args) {
65
- await this.#hooks.runner("onSourceFileChanged").run(...args);
66
- }
67
- /**
68
- * When the build process is starting
69
- */
70
- async onBuildStarting(...args) {
71
- await this.#hooks.runner("onBuildStarting").run(...args);
72
- }
73
- /**
74
- * When the build process is completed
75
- */
76
- async onBuildCompleted(...args) {
77
- await this.#hooks.runner("onBuildCompleted").run(...args);
78
- }
79
- };
80
-
81
- // src/helpers.ts
14
+ // src/utils.ts
15
+ import Cache from "tmp-cache";
82
16
  import { isJunk } from "junk";
83
17
  import fastGlob from "fast-glob";
18
+ import Hooks from "@poppinss/hooks";
19
+ import { existsSync } from "fs";
84
20
  import getRandomPort from "get-port";
85
- import { existsSync } from "node:fs";
86
- import { fileURLToPath } from "node:url";
21
+ import { fileURLToPath } from "url";
87
22
  import { execaNode, execa } from "execa";
88
- import { copyFile, mkdir } from "node:fs/promises";
23
+ import { importDefault } from "@poppinss/utils";
24
+ import { copyFile, mkdir } from "fs/promises";
89
25
  import { EnvLoader, EnvParser } from "@adonisjs/env";
90
- import { ConfigParser, Watcher } from "@poppinss/chokidar-ts";
91
- import { basename, dirname, isAbsolute, join, relative } from "node:path";
92
-
93
- // src/debug.ts
94
- import { debuglog } from "node:util";
95
- var debug_default = debuglog("adonisjs:assembler");
96
-
97
- // src/helpers.ts
98
- var DEFAULT_NODE_ARGS = [
99
- // Use ts-node/esm loader. The project must install it
100
- process.versions.tsNodeMaintained ? "--import=ts-node-maintained/register/esm" : "--loader=ts-node/esm",
101
- // Enable source maps, since TSNode source maps are broken
102
- "--enable-source-maps"
103
- ];
104
- if (process.allowedNodeEnvironmentFlags.has("--disable-warning")) {
105
- DEFAULT_NODE_ARGS.push("--disable-warning=ExperimentalWarning");
106
- } else {
107
- DEFAULT_NODE_ARGS.push("--no-warnings");
108
- }
26
+ import chokidar from "chokidar";
27
+ import { basename, dirname, isAbsolute, join, relative } from "path";
28
+ var DEFAULT_NODE_ARGS = ["--import=@poppinss/ts-exec", "--enable-source-maps"];
109
29
  function parseConfig(cwd, ts) {
110
- const { config, error } = new ConfigParser(cwd, "tsconfig.json", ts).parse();
111
- if (error) {
30
+ const cwdPath = typeof cwd === "string" ? cwd : fileURLToPath(cwd);
31
+ const configFile = join(cwdPath, "tsconfig.json");
32
+ debug_default('parsing config file "%s"', configFile);
33
+ let hardException = null;
34
+ const parsedConfig = ts.getParsedCommandLineOfConfigFile(
35
+ configFile,
36
+ {},
37
+ {
38
+ ...ts.sys,
39
+ useCaseSensitiveFileNames: true,
40
+ getCurrentDirectory: () => cwdPath,
41
+ onUnRecoverableConfigFileDiagnostic: (error) => hardException = error
42
+ }
43
+ );
44
+ if (hardException) {
112
45
  const compilerHost = ts.createCompilerHost({});
113
- console.log(ts.formatDiagnosticsWithColorAndContext([error], compilerHost));
46
+ console.log(ts.formatDiagnosticsWithColorAndContext([hardException], compilerHost));
114
47
  return;
115
48
  }
116
- if (config.errors.length) {
49
+ if (parsedConfig.errors.length) {
117
50
  const compilerHost = ts.createCompilerHost({});
118
- console.log(ts.formatDiagnosticsWithColorAndContext(config.errors, compilerHost));
51
+ console.log(ts.formatDiagnosticsWithColorAndContext(parsedConfig.errors, compilerHost));
119
52
  return;
120
53
  }
121
- return config;
54
+ return parsedConfig;
122
55
  }
123
56
  function runNode(cwd, options) {
124
57
  const childProcess = execaNode(options.script, options.scriptArgs, {
@@ -152,20 +85,8 @@ function run(cwd, options) {
152
85
  });
153
86
  return childProcess;
154
87
  }
155
- function watch(cwd, ts, options) {
156
- const config = parseConfig(cwd, ts);
157
- if (!config) {
158
- return;
159
- }
160
- const watcher = new Watcher(typeof cwd === "string" ? cwd : fileURLToPath(cwd), config);
161
- const chokidar = watcher.watch(["."], { usePolling: options.poll });
162
- return { watcher, chokidar };
163
- }
164
- function isDotEnvFile(filePath) {
165
- if (filePath === ".env") {
166
- return true;
167
- }
168
- return filePath.includes(".env.");
88
+ function watch(options) {
89
+ return chokidar.watch(["."], options);
169
90
  }
170
91
  async function getPort(cwd) {
171
92
  if (process.env.PORT) {
@@ -207,9 +128,57 @@ async function copyFiles(files, cwd, outDir) {
207
128
  });
208
129
  return await Promise.all(copyPromises);
209
130
  }
131
+ function memoize(fn, maxKeys) {
132
+ const cache = new Cache({ max: maxKeys });
133
+ return (input) => {
134
+ if (cache.has(input)) {
135
+ return cache.get(input);
136
+ }
137
+ return fn(input);
138
+ };
139
+ }
140
+ async function loadHooks(rcFileHooks, names) {
141
+ const groups = names.map((name) => {
142
+ return {
143
+ group: name,
144
+ hooks: rcFileHooks?.[name] ?? []
145
+ };
146
+ });
147
+ const hooks = new Hooks();
148
+ for (const { group, hooks: collection } of groups) {
149
+ for (const item of collection) {
150
+ hooks.add(group, await importDefault(item));
151
+ }
152
+ }
153
+ return hooks;
154
+ }
155
+ function throttle(fn, name) {
156
+ name = name || "throttled";
157
+ let isBusy = false;
158
+ let hasQueuedCalls = false;
159
+ let lastCallArgs;
160
+ async function throttled(...args) {
161
+ if (isBusy) {
162
+ debug_default('ignoring "%s" invocation as current execution is in progress', name);
163
+ hasQueuedCalls = true;
164
+ lastCallArgs = args;
165
+ return;
166
+ }
167
+ isBusy = true;
168
+ debug_default('executing "%s" function', name);
169
+ await fn(...args);
170
+ isBusy = false;
171
+ if (hasQueuedCalls) {
172
+ hasQueuedCalls = false;
173
+ debug_default('resuming and running latest "%s" invocation', name);
174
+ await throttled(...lastCallArgs);
175
+ }
176
+ }
177
+ return throttled;
178
+ }
210
179
 
211
180
  // src/bundler.ts
212
- var SUPPORT_PACKAGE_MANAGERS = {
181
+ var SUPPORTED_PACKAGE_MANAGERS = {
213
182
  "npm": {
214
183
  packageManagerFiles: ["package-lock.json"],
215
184
  installCommand: 'npm ci --omit="dev"'
@@ -231,33 +200,26 @@ var SUPPORT_PACKAGE_MANAGERS = {
231
200
  installCommand: "bun install --production"
232
201
  }
233
202
  };
234
- var ui = cliui();
235
203
  var Bundler = class {
236
- #cwd;
204
+ constructor(cwd, ts, options) {
205
+ this.cwd = cwd;
206
+ this.options = options;
207
+ this.#cwdPath = fileURLToPath2(this.cwd);
208
+ this.#ts = ts;
209
+ }
237
210
  #cwdPath;
238
211
  #ts;
239
- #logger = ui.logger;
240
- #hooks;
241
- #options;
242
212
  /**
243
- * Getting reference to colors library from logger
213
+ * Hooks to execute custom actions during the build process
244
214
  */
245
- get #colors() {
246
- return this.#logger.getColors();
247
- }
248
- constructor(cwd, ts, options) {
249
- this.#cwd = cwd;
250
- this.#cwdPath = fileURLToPath2(this.#cwd);
251
- this.#ts = ts;
252
- this.#options = options;
253
- this.#hooks = new AssemblerHooks(options.hooks);
254
- }
215
+ #hooks;
216
+ ui = cliui();
255
217
  /**
256
218
  * Returns the relative unix path for an absolute
257
219
  * file path
258
220
  */
259
221
  #getRelativeName(filePath) {
260
- return slash(relative2(this.#cwdPath, filePath));
222
+ return string.toUnixSlash(relative2(this.#cwdPath, filePath));
261
223
  }
262
224
  /**
263
225
  * Cleans up the build directory
@@ -265,33 +227,12 @@ var Bundler = class {
265
227
  async #cleanupBuildDirectory(outDir) {
266
228
  await fs.rm(outDir, { recursive: true, force: true, maxRetries: 5 });
267
229
  }
268
- /**
269
- * Runs assets bundler command to build assets
270
- */
271
- async #buildAssets() {
272
- const assetsBundler = this.#options.assets;
273
- if (!assetsBundler?.enabled) {
274
- return true;
275
- }
276
- try {
277
- this.#logger.info("compiling frontend assets", { suffix: assetsBundler.cmd });
278
- await run(this.#cwd, {
279
- stdio: "inherit",
280
- reject: true,
281
- script: assetsBundler.cmd,
282
- scriptArgs: assetsBundler.args
283
- });
284
- return true;
285
- } catch {
286
- return false;
287
- }
288
- }
289
230
  /**
290
231
  * Runs tsc command to build the source.
291
232
  */
292
233
  async #runTsc(outDir) {
293
234
  try {
294
- await run(this.#cwd, {
235
+ await run(this.cwd, {
295
236
  stdio: "inherit",
296
237
  script: "tsc",
297
238
  scriptArgs: ["--outDir", outDir]
@@ -305,26 +246,21 @@ var Bundler = class {
305
246
  * Copy meta files to the output directory
306
247
  */
307
248
  async #copyMetaFiles(outDir, additionalFilesToCopy) {
308
- const metaFiles = (this.#options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy);
249
+ const metaFiles = (this.options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy);
309
250
  await copyFiles(metaFiles, this.#cwdPath, outDir);
310
251
  }
311
252
  /**
312
253
  * Detect the package manager used by the project
313
- * and return the lockfile name and install command
314
- * related to it.
315
254
  */
316
- async #getPackageManager(client) {
317
- let pkgManager = client;
318
- if (!pkgManager) {
319
- pkgManager = await detectPackageManager(this.#cwdPath);
320
- }
321
- if (!pkgManager) {
322
- pkgManager = "npm";
255
+ async #detectPackageManager() {
256
+ const pkgManager = await detectPackageManager(this.#cwdPath);
257
+ if (pkgManager === "deno") {
258
+ return "npm";
323
259
  }
324
- if (!Object.keys(SUPPORT_PACKAGE_MANAGERS).includes(pkgManager)) {
325
- return null;
260
+ if (pkgManager === "pnpm@6") {
261
+ return "pnpm";
326
262
  }
327
- return SUPPORT_PACKAGE_MANAGERS[pkgManager];
263
+ return pkgManager;
328
264
  }
329
265
  /**
330
266
  * Rewrite the ace file since the original one
@@ -346,258 +282,352 @@ var Bundler = class {
346
282
  `
347
283
  );
348
284
  await fs.writeFile(aceFileLocation, aceFileContent);
349
- this.#logger.info("rewrited ace file", { suffix: this.#getRelativeName(aceFileLocation) });
350
- }
351
- /**
352
- * Set a custom CLI UI logger
353
- */
354
- setLogger(logger) {
355
- this.#logger = logger;
356
- return this;
285
+ this.ui.logger.info("created ace file", { suffix: this.#getRelativeName(aceFileLocation) });
357
286
  }
358
287
  /**
359
288
  * Bundles the application to be run in production
360
289
  */
361
290
  async bundle(stopOnError = true, client) {
362
- await this.#hooks.registerBuildHooks();
363
- const config = parseConfig(this.#cwd, this.#ts);
291
+ this.#hooks = await loadHooks(this.options.hooks, ["buildStarting", "buildFinished"]);
292
+ this.packageManager = client ?? await this.#detectPackageManager() ?? "npm";
293
+ const config = parseConfig(this.cwd, this.#ts);
364
294
  if (!config) {
365
295
  return false;
366
296
  }
367
- const outDir = config.options.outDir || fileURLToPath2(new URL("build/", this.#cwd));
368
- this.#logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
297
+ const outDir = config.options.outDir || fileURLToPath2(new URL("build/", this.cwd));
298
+ this.ui.logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
369
299
  await this.#cleanupBuildDirectory(outDir);
370
- if (!await this.#buildAssets()) {
371
- return false;
372
- }
373
- await this.#hooks.onBuildStarting({ colors: ui.colors, logger: this.#logger });
374
- this.#logger.info("compiling typescript source", { suffix: "tsc" });
300
+ await this.#hooks.runner("buildStarting").run(this);
301
+ this.ui.logger.info("compiling typescript source", { suffix: "tsc" });
375
302
  const buildCompleted = await this.#runTsc(outDir);
376
303
  await this.#createAceFile(outDir);
377
304
  if (!buildCompleted && stopOnError) {
378
305
  await this.#cleanupBuildDirectory(outDir);
379
- const instructions = ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
306
+ const instructions = this.ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
380
307
  instructions.add(
381
- this.#colors.red("Cannot complete the build process as there are TypeScript errors.")
308
+ this.ui.colors.red("Cannot complete the build process as there are TypeScript errors.")
382
309
  );
383
310
  instructions.add(
384
- this.#colors.red(
311
+ this.ui.colors.red(
385
312
  'Use "--ignore-ts-errors" flag to ignore TypeScript errors and continue the build.'
386
313
  )
387
314
  );
388
- this.#logger.logError(instructions.prepare());
315
+ this.ui.logger.logError(instructions.prepare());
389
316
  return false;
390
317
  }
391
- const pkgManager = await this.#getPackageManager(client);
392
- const pkgFiles = pkgManager ? ["package.json", ...pkgManager.packageManagerFiles] : ["package.json"];
393
- this.#logger.info("copying meta files to the output directory");
318
+ const pkgFiles = [
319
+ "package.json",
320
+ ...SUPPORTED_PACKAGE_MANAGERS[this.packageManager].packageManagerFiles
321
+ ];
322
+ this.ui.logger.info("copying meta files to the output directory");
394
323
  await this.#copyMetaFiles(outDir, pkgFiles);
395
- this.#logger.success("build completed");
396
- this.#logger.log("");
397
- await this.#hooks.onBuildCompleted({ colors: ui.colors, logger: this.#logger });
398
- ui.instructions().useRenderer(this.#logger.getRenderer()).heading("Run the following commands to start the server in production").add(this.#colors.cyan(`cd ${this.#getRelativeName(outDir)}`)).add(
399
- this.#colors.cyan(
400
- pkgManager ? pkgManager.installCommand : "Install production dependencies"
401
- )
402
- ).add(this.#colors.cyan("node bin/server.js")).render();
324
+ this.ui.logger.success("build completed");
325
+ this.ui.logger.log("");
326
+ const displayMessage = this.ui.instructions().heading("Run the following commands to start the server in production");
327
+ await this.#hooks.runner("buildFinished").run(this, displayMessage);
328
+ 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();
403
329
  return true;
404
330
  }
405
331
  };
406
332
 
407
333
  // src/dev_server.ts
408
- import picomatch from "picomatch";
409
- import { relative as relative3 } from "node:path";
334
+ import { relative as relative4 } from "path";
335
+ import { cliui as cliui2 } from "@poppinss/cliui";
410
336
  import prettyHrtime from "pretty-hrtime";
411
- import { fileURLToPath as fileURLToPath3 } from "node:url";
412
- import { cliui as cliui3 } from "@poppinss/cliui";
337
+ import { fileURLToPath as fileURLToPath4 } from "url";
338
+ import string3 from "@poppinss/utils/string";
413
339
 
414
- // src/assets_dev_server.ts
415
- import { cliui as cliui2 } from "@poppinss/cliui";
416
- var ui2 = cliui2();
417
- var AssetsDevServer = class {
340
+ // src/file_system.ts
341
+ import picomatch from "picomatch";
342
+ import { fileURLToPath as fileURLToPath3 } from "url";
343
+ import { join as join3, relative as relative3 } from "path";
344
+ import string2 from "@poppinss/utils/string";
345
+ var DEFAULT_INCLUDES = ["**/*"];
346
+ var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**"];
347
+ var DEFAULT_EXCLUDES = ["node_modules/**", "bower_components/**", "jspm_packages/**"];
348
+ var FileSystem = class {
349
+ /**
350
+ * The current working project directory
351
+ */
418
352
  #cwd;
419
- #logger = ui2.logger;
420
- #options;
421
- #devServer;
422
353
  /**
423
- * Getting reference to colors library from logger
354
+ * Referenced to the parsed ts config file. We use it to read the includes,
355
+ * excludes and pre-scanned files.
424
356
  */
425
- get #colors() {
426
- return this.#logger.getColors();
427
- }
428
- constructor(cwd, options) {
429
- this.#cwd = cwd;
430
- this.#options = options;
431
- }
357
+ #tsConfig;
432
358
  /**
433
- * Logs messages from vite dev server stdout and stderr
359
+ * Set of pre-scanned typeScript files provided by tsconfig
434
360
  */
435
- #logViteDevServerMessage(data) {
436
- const dataString = data.toString();
437
- const lines = dataString.split("\n");
438
- if (dataString.includes("Local") && dataString.includes("Network")) {
439
- const sticker = ui2.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer());
440
- lines.forEach((line) => {
441
- if (line.trim()) {
442
- sticker.add(line);
443
- }
444
- });
445
- sticker.render();
446
- return;
447
- }
448
- if (dataString.includes("ready in")) {
449
- console.log("");
450
- console.log(dataString.trim());
451
- return;
452
- }
453
- lines.forEach((line) => {
454
- if (line.trim()) {
455
- console.log(line);
456
- }
457
- });
458
- }
361
+ #scannedTypeScriptFiles = /* @__PURE__ */ new Set();
459
362
  /**
460
- * Logs messages from assets dev server stdout and stderr
363
+ * Picomatch matcher function to know if a file path is
364
+ * part of includes
461
365
  */
462
- #logAssetsDevServerMessage(data) {
463
- const dataString = data.toString();
464
- const lines = dataString.split("\n");
465
- lines.forEach((line) => {
466
- if (line.trim()) {
467
- console.log(line);
468
- }
469
- });
470
- }
366
+ #isIncluded;
471
367
  /**
472
- * Set a custom CLI UI logger
368
+ * Picomatch matcher function to know if a file path is
369
+ * part of excludes
473
370
  */
474
- setLogger(logger) {
475
- this.#logger = logger;
476
- return this;
371
+ #isExcluded;
372
+ /**
373
+ * Picomatch matcher function to know if a file path is a
374
+ * meta file with reloadServer option enabled
375
+ */
376
+ #isMetaFileWithReloadsEnabled;
377
+ /**
378
+ * Picomatch matcher function to know if a file path is a
379
+ * meta file with reloadServer option disabled
380
+ */
381
+ #isMetaFileWithReloadsDisabled;
382
+ /**
383
+ * Picomatch matcher function to know if a file path is a
384
+ * test file or not
385
+ */
386
+ #isTestFile;
387
+ /**
388
+ * References to includes and excludes
389
+ */
390
+ #includes;
391
+ #excludes;
392
+ /**
393
+ * Includes glob patterns extracted from "tsconfig.json" file.
394
+ * Defaults to: ["**\/*"]
395
+ */
396
+ get includes() {
397
+ return this.#includes;
477
398
  }
478
399
  /**
479
- * Starts the assets bundler server. The assets bundler server process is
480
- * considered as the secondary process and therefore we do not perform
481
- * any cleanup if it dies.
400
+ * Excludes glob patterns extracted from "tsconfig.json" file.
401
+ *
402
+ * Defaults to: [
403
+ * 'node_modules/**',
404
+ * 'bower_components/**',
405
+ * 'jspm_packages/**,
406
+ * ]
407
+ *
408
+ * Following patterns are always ignored
409
+ *
410
+ * '.git/**', 'coverage/**', '.github/**'
411
+ */
412
+ get excludes() {
413
+ return this.#excludes;
414
+ }
415
+ /**
416
+ * Inspect a relative path to find its source in the project
417
+ */
418
+ inspect = memoize((filePath) => {
419
+ const relativePath = filePath;
420
+ const absolutePath = string2.toUnixSlash(join3(this.#cwd, relativePath));
421
+ if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
422
+ debug_default('backend project file "%s"', relativePath);
423
+ const isTestFile = this.#isTestFile(relativePath);
424
+ return {
425
+ fileType: isTestFile ? "test" : "script",
426
+ reloadServer: !isTestFile,
427
+ unixRelativePath: relativePath,
428
+ unixAbsolutePath: absolutePath
429
+ };
430
+ }
431
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
432
+ debug_default('meta file "%s"', relativePath);
433
+ return {
434
+ fileType: "meta",
435
+ reloadServer: true,
436
+ unixRelativePath: relativePath,
437
+ unixAbsolutePath: absolutePath
438
+ };
439
+ }
440
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
441
+ debug_default('meta file "%s"', relativePath);
442
+ return {
443
+ fileType: "meta",
444
+ reloadServer: false,
445
+ unixRelativePath: relativePath,
446
+ unixAbsolutePath: absolutePath
447
+ };
448
+ }
449
+ debug_default('ignored file "%s"', relativePath);
450
+ return null;
451
+ });
452
+ /**
453
+ * Returns true if the directory should be watched. Chokidar sends
454
+ * absolute unix paths to the ignored callback.
455
+ *
456
+ * You must use "shouldWatchFile" method for files and call this method
457
+ * for directories only.
482
458
  */
483
- start() {
484
- if (!this.#options?.enabled) {
485
- return;
459
+ shouldWatchDirectory = memoize((absolutePath) => {
460
+ if (absolutePath === this.#cwd) {
461
+ debug_default("watching project root");
462
+ return true;
486
463
  }
487
- this.#logger.info(`starting "${this.#options.driver}" dev server...`);
488
- this.#devServer = run(this.#cwd, {
489
- script: this.#options.cmd,
490
- reject: true,
491
- /**
492
- * We do not inherit the stdio for vite and encore, because in
493
- * inherit mode they own the stdin and interrupts the
494
- * `Ctrl + C` command.
495
- */
496
- stdio: "pipe",
497
- scriptArgs: this.#options.args
498
- });
499
- this.#devServer.stdout?.on("data", (data) => {
500
- if (this.#options.driver === "vite") {
501
- this.#logViteDevServerMessage(data);
502
- } else {
503
- this.#logAssetsDevServerMessage(data);
464
+ const relativePath = string2.toUnixSlash(relative3(this.#cwd, absolutePath));
465
+ if (this.#isExcluded(relativePath)) {
466
+ debug_default('watching "%s"', absolutePath);
467
+ return false;
468
+ }
469
+ return true;
470
+ });
471
+ constructor(cwd, tsConfig, rcFile) {
472
+ this.#cwd = string2.toUnixSlash(typeof cwd === "string" ? cwd : fileURLToPath3(cwd));
473
+ this.#tsConfig = tsConfig;
474
+ const files = tsConfig.fileNames;
475
+ const metaFiles = rcFile.metaFiles ?? [];
476
+ const testSuites = rcFile.suites ?? [];
477
+ const outDir = tsConfig.raw.compilerOptions?.outDir;
478
+ files.forEach((file) => this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file)));
479
+ this.#includes = tsConfig.raw.include || DEFAULT_INCLUDES;
480
+ this.#excludes = ALWAYS_EXCLUDE.concat(
481
+ tsConfig.raw.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES)
482
+ );
483
+ this.#isMetaFileWithReloadsEnabled = picomatch(
484
+ metaFiles.filter((file) => !!file.reloadServer).map((file) => file.pattern),
485
+ {
486
+ cwd: this.#cwd
504
487
  }
505
- });
506
- this.#devServer.stderr?.on("data", (data) => {
507
- if (this.#options.driver === "vite") {
508
- this.#logViteDevServerMessage(data);
509
- } else {
510
- this.#logAssetsDevServerMessage(data);
488
+ );
489
+ this.#isMetaFileWithReloadsDisabled = picomatch(
490
+ metaFiles.filter((file) => !file.reloadServer).map((file) => file.pattern),
491
+ {
492
+ cwd: this.#cwd
511
493
  }
494
+ );
495
+ this.#isTestFile = picomatch(
496
+ testSuites.flatMap((suite) => suite.files),
497
+ {
498
+ cwd: this.#cwd
499
+ }
500
+ );
501
+ this.#isIncluded = picomatch(this.#includes, {
502
+ cwd: this.#cwd
512
503
  });
513
- this.#devServer.then((result) => {
514
- this.#logger.warning(
515
- `"${this.#options.driver}" dev server closed with status code "${result.exitCode}"`
516
- );
517
- }).catch((error) => {
518
- this.#logger.warning(`unable to connect to "${this.#options.driver}" dev server`);
519
- this.#logger.fatal(error);
504
+ this.#isExcluded = picomatch(this.#excludes, {
505
+ cwd: this.#cwd
506
+ });
507
+ debug_default("initiating file system %O", {
508
+ includes: this.#includes,
509
+ excludes: this.#includes,
510
+ outDir,
511
+ files,
512
+ metaFiles,
513
+ testSuites
520
514
  });
521
515
  }
522
516
  /**
523
- * Stop the dev server
517
+ * Returns a boolean telling if a file path is a script file or not.
518
+ *
519
+ * - Files ending with ".ts", ".tsx" are considered are script files.
520
+ * - Files ending with ".js" with "allowJs" option enabled are considered are script files.
521
+ * - Files ending with ".json" with "resolveJsonModule" option enabled are considered are script files.
524
522
  */
525
- stop() {
526
- if (this.#devServer) {
527
- this.#devServer.removeAllListeners();
528
- this.#devServer.kill("SIGKILL");
529
- this.#devServer = void 0;
523
+ #isScriptFile(relativePath) {
524
+ if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
525
+ return true;
526
+ }
527
+ if (this.#tsConfig.options.allowJs && relativePath.endsWith(".js")) {
528
+ return true;
530
529
  }
530
+ if (this.#tsConfig.options.resolveJsonModule && relativePath.endsWith(".json")) {
531
+ return true;
532
+ }
533
+ return false;
534
+ }
535
+ /**
536
+ * Check if the file path is part of the backend TypeScript project. We use
537
+ * tsconfig "includes", "excludes", and "files" paths to test if the file
538
+ * should be considered or not.
539
+ */
540
+ #isPartOfBackendProject(relativePath) {
541
+ if (this.#isExcluded(relativePath)) {
542
+ debug_default('excluded by tsconfig "%s"', relativePath);
543
+ return false;
544
+ }
545
+ if (this.#isIncluded(relativePath)) {
546
+ debug_default('included by tsconfig "%s"', relativePath);
547
+ return true;
548
+ }
549
+ return false;
550
+ }
551
+ /**
552
+ * Returns true if the file should be watched. Chokidar sends
553
+ * absolute unix paths to the ignored callback.
554
+ *
555
+ * You must use "shouldWatchDirectory" method for directories and call
556
+ * this method for files only.
557
+ */
558
+ shouldWatchFile(absolutePath) {
559
+ return this.inspect(relative3(this.#cwd, absolutePath)) !== null;
531
560
  }
532
561
  };
533
562
 
534
563
  // src/dev_server.ts
535
- var ui3 = cliui3();
536
564
  var DevServer = class {
537
- #cwd;
538
- #logger = ui3.logger;
539
- #options;
565
+ constructor(cwd, options) {
566
+ this.cwd = cwd;
567
+ this.options = options;
568
+ this.#cwdPath = fileURLToPath4(this.cwd);
569
+ }
540
570
  /**
541
- * Flag to know if the dev server is running in watch
542
- * mode
571
+ * File path computed from the cwd
543
572
  */
544
- #isWatching = false;
573
+ #cwdPath;
545
574
  /**
546
- * Script file to start the development server
575
+ * External listeners that are invoked when child process
576
+ * gets an error or closes
547
577
  */
548
- #scriptFile = "bin/server.js";
578
+ #onError;
579
+ #onClose;
549
580
  /**
550
- * Picomatch matcher function to know if a file path is a
551
- * meta file with reloadServer option enabled
581
+ * The stickyPort is set by the start and the startAndWatch methods
582
+ * and we will continue to use that port during restart
552
583
  */
553
- #isMetaFileWithReloadsEnabled;
584
+ #stickyPort;
554
585
  /**
555
- * Picomatch matcher function to know if a file path is a
556
- * meta file with reloadServer option disabled
586
+ * The mode is set by the start and the startAndWatch methods
557
587
  */
558
- #isMetaFileWithReloadsDisabled;
588
+ #mode = "static";
559
589
  /**
560
- * External listeners that are invoked when child process
561
- * gets an error or closes
590
+ * Reference to chokidar watcher
562
591
  */
563
- #onError;
564
- #onClose;
592
+ #watcher;
565
593
  /**
566
594
  * Reference to the child process
567
595
  */
568
596
  #httpServer;
569
597
  /**
570
- * Reference to the watcher
571
- */
572
- #watcher;
573
- /**
574
- * Reference to the assets server
598
+ * Filesystem is used to decide which files to watch or entertain when
599
+ * using hot-hook
575
600
  */
576
- #assetsServer;
601
+ #fileSystem;
577
602
  /**
578
603
  * Hooks to execute custom actions during the dev server lifecycle
579
604
  */
580
605
  #hooks;
581
606
  /**
582
- * Getting reference to colors library from logger
607
+ * Restarts the HTTP server and throttle concurrent calls to
608
+ * ensure we do not end up with a long loop of restarts
583
609
  */
584
- get #colors() {
585
- return this.#logger.getColors();
586
- }
587
- constructor(cwd, options) {
588
- this.#cwd = cwd;
589
- this.#options = options;
590
- this.#hooks = new AssemblerHooks(options.hooks);
591
- if (this.#options.hmr) {
592
- this.#options.nodeArgs = this.#options.nodeArgs.concat(["--import=hot-hook/register"]);
610
+ #restartHTTPServer = throttle(async () => {
611
+ if (this.#httpServer) {
612
+ this.#httpServer.removeAllListeners();
613
+ this.#httpServer.kill("SIGKILL");
593
614
  }
594
- this.#isMetaFileWithReloadsEnabled = picomatch(
595
- (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer === true).map(({ pattern }) => pattern)
596
- );
597
- this.#isMetaFileWithReloadsDisabled = picomatch(
598
- (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer !== true).map(({ pattern }) => pattern)
599
- );
615
+ await this.#startHTTPServer(this.#stickyPort);
616
+ }, "restartHTTPServer");
617
+ /**
618
+ * CLI UI to log colorful messages
619
+ */
620
+ ui = cliui2();
621
+ /**
622
+ * The mode in which the DevServer is running.
623
+ */
624
+ get mode() {
625
+ return this.#mode;
600
626
  }
627
+ /**
628
+ * Script file to start the development server
629
+ */
630
+ scriptFile = "bin/server.ts";
601
631
  /**
602
632
  * Inspect if child process message is from AdonisJS HTTP server
603
633
  */
@@ -605,7 +635,25 @@ var DevServer = class {
605
635
  return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
606
636
  }
607
637
  /**
608
- * Inspect if child process message is coming from Hot Hook
638
+ * Displays the server info and executes the hooks after the server has been
639
+ * started.
640
+ */
641
+ async #postServerReady(message) {
642
+ const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
643
+ const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(`http://${host}:${message.port}`)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
644
+ if (message.duration) {
645
+ displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
646
+ }
647
+ try {
648
+ await this.#hooks.runner("devServerStarted").run(this, displayMessage);
649
+ } catch (error) {
650
+ this.ui.logger.error('One of the "devServerStarted" hooks failed');
651
+ this.ui.logger.fatal(error);
652
+ }
653
+ displayMessage.render();
654
+ }
655
+ /**
656
+ * Inspect if child process message is coming from hot-hook
609
657
  */
610
658
  #isHotHookMessage(message) {
611
659
  return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
@@ -614,121 +662,130 @@ var DevServer = class {
614
662
  * Conditionally clear the terminal screen
615
663
  */
616
664
  #clearScreen() {
617
- if (this.#options.clearScreen) {
665
+ if (this.options.clearScreen) {
618
666
  process.stdout.write("\x1Bc");
619
667
  }
620
668
  }
621
669
  /**
622
- * Starts the HTTP server
623
- */
624
- #startHTTPServer(port, mode) {
625
- const hooksArgs = { colors: this.#colors, logger: this.#logger };
626
- this.#httpServer = runNode(this.#cwd, {
627
- script: this.#scriptFile,
628
- env: { PORT: port, ...this.#options.env },
629
- nodeArgs: this.#options.nodeArgs,
630
- reject: true,
631
- scriptArgs: this.#options.scriptArgs
632
- });
633
- this.#httpServer.on("message", async (message) => {
634
- if (this.#isHotHookMessage(message)) {
635
- const path = relative3(fileURLToPath3(this.#cwd), message.path || message.paths?.[0]);
636
- this.#hooks.onSourceFileChanged(hooksArgs, path);
637
- if (message.type === "hot-hook:full-reload") {
638
- this.#clearScreen();
639
- this.#logger.log(`${this.#colors.green("full-reload")} ${path}`);
640
- this.#restartHTTPServer(port);
641
- this.#hooks.onDevServerStarted(hooksArgs);
642
- } else if (message.type === "hot-hook:invalidated") {
643
- this.#logger.log(`${this.#colors.green("invalidated")} ${path}`);
644
- }
645
- }
646
- if (this.#isAdonisJSReadyMessage(message)) {
647
- const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
648
- const displayMessage = ui3.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer()).add(`Server address: ${this.#colors.cyan(`http://${host}:${message.port}`)}`);
649
- const watchMode = this.#options.hmr ? "HMR" : this.#isWatching ? "Legacy" : "None";
650
- displayMessage.add(`Watch Mode: ${this.#colors.cyan(watchMode)}`);
651
- if (message.duration) {
652
- displayMessage.add(`Ready in: ${this.#colors.cyan(prettyHrtime(message.duration))}`);
653
- }
654
- displayMessage.render();
655
- await this.#hooks.onDevServerStarted({ colors: ui3.colors, logger: this.#logger });
656
- }
657
- });
658
- this.#httpServer.then((result) => {
659
- if (mode === "nonblocking") {
660
- this.#onClose?.(result.exitCode);
661
- this.#watcher?.close();
662
- this.#assetsServer?.stop();
663
- } else {
664
- this.#logger.info("Underlying HTTP server closed. Still watching for changes");
665
- }
666
- }).catch((error) => {
667
- if (mode === "nonblocking") {
668
- this.#onError?.(error);
669
- this.#watcher?.close();
670
- this.#assetsServer?.stop();
671
- } else {
672
- this.#logger.info("Underlying HTTP server died. Still watching for changes");
673
- }
674
- });
675
- }
676
- /**
677
- * Starts the assets server
678
- */
679
- #startAssetsServer() {
680
- this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
681
- this.#assetsServer.setLogger(this.#logger);
682
- this.#assetsServer.start();
683
- }
684
- /**
685
- * Restarts the HTTP server in the watch mode. Do not call this
686
- * method when not in watch mode
670
+ * Handles file change event
687
671
  */
688
- #restartHTTPServer(port) {
689
- if (this.#httpServer) {
690
- this.#httpServer.removeAllListeners();
691
- this.#httpServer.kill("SIGKILL");
672
+ #handleFileChange(filePath, action, info) {
673
+ if ((action === "add" || action === "delete") && this.mode === "hmr") {
674
+ debug_default("ignoring add and delete actions in HMR mode %s", filePath);
675
+ return;
692
676
  }
693
- this.#startHTTPServer(port, "blocking");
694
- }
695
- /**
696
- * Handles a non TypeScript file change
697
- */
698
- #handleFileChange(action, port, relativePath) {
699
- if (isDotEnvFile(relativePath)) {
700
- this.#clearScreen();
701
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
702
- this.#restartHTTPServer(port);
677
+ if (info && info.source === "hot-hook" && info.hotReloaded) {
678
+ debug_default("hot reloading %s, info %O", filePath, info);
679
+ this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${filePath}`);
703
680
  return;
704
681
  }
705
- if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
706
- this.#clearScreen();
707
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
708
- this.#restartHTTPServer(port);
682
+ if (info && !info.fullReload) {
683
+ debug_default("ignoring full reload", filePath, info);
709
684
  return;
710
685
  }
711
- if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
686
+ const file = this.#fileSystem.inspect(filePath);
687
+ if (!file) {
688
+ return;
689
+ }
690
+ if (file.reloadServer) {
712
691
  this.#clearScreen();
713
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
692
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
693
+ this.#restartHTTPServer();
694
+ return;
714
695
  }
696
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
715
697
  }
716
698
  /**
717
- * Handles TypeScript source file change
699
+ * Registers inline hooks for the file changes and restarts the
700
+ * HTTP server when a file gets changed.
718
701
  */
719
- async #handleSourceFileChange(action, port, relativePath) {
720
- await this.#hooks.onSourceFileChanged({ colors: ui3.colors, logger: this.#logger }, relativePath);
721
- this.#clearScreen();
722
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
723
- this.#restartHTTPServer(port);
702
+ #registerServerRestartHooks() {
703
+ this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
704
+ this.#hooks.add(
705
+ "fileChanged",
706
+ (filePath, info) => this.#handleFileChange(filePath, "update", info)
707
+ );
708
+ this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
724
709
  }
725
710
  /**
726
- * Set a custom CLI UI logger
711
+ * Starts the HTTP server
727
712
  */
728
- setLogger(logger) {
729
- this.#logger = logger;
730
- this.#assetsServer?.setLogger(logger);
731
- return this;
713
+ async #startHTTPServer(port) {
714
+ await this.#hooks.runner("devServerStarting").run(this);
715
+ debug_default('starting http server using "%s" file, options %O', this.scriptFile, this.options);
716
+ return new Promise(async (resolve) => {
717
+ this.#httpServer = runNode(this.cwd, {
718
+ script: this.scriptFile,
719
+ env: { PORT: port, ...this.options.env },
720
+ nodeArgs: this.options.nodeArgs,
721
+ reject: true,
722
+ scriptArgs: this.options.scriptArgs
723
+ });
724
+ this.#httpServer.on("message", async (message) => {
725
+ if (this.#isAdonisJSReadyMessage(message)) {
726
+ debug_default("received http server ready message %O", message);
727
+ await this.#postServerReady(message);
728
+ resolve();
729
+ } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
730
+ debug_default("received hot-hook message %O", message);
731
+ if (message.type === "hot-hook:file-changed") {
732
+ switch (message.action) {
733
+ case "add":
734
+ this.#hooks.runner("fileAdded").run(string3.toUnixSlash(relative4(this.#cwdPath, message.path)), this);
735
+ break;
736
+ case "change":
737
+ this.#hooks.runner("fileChanged").run(
738
+ string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
739
+ {
740
+ source: "hot-hook",
741
+ fullReload: false,
742
+ hotReloaded: false
743
+ },
744
+ this
745
+ );
746
+ break;
747
+ case "unlink":
748
+ this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(relative4(this.#cwdPath, message.path)), this);
749
+ }
750
+ } else if (message.type === "hot-hook:full-reload") {
751
+ this.#hooks.runner("fileChanged").run(
752
+ string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
753
+ {
754
+ source: "hot-hook",
755
+ fullReload: true,
756
+ hotReloaded: false
757
+ },
758
+ this
759
+ );
760
+ } else if (message.type === "hot-hook:invalidated") {
761
+ this.#hooks.runner("fileChanged").run(
762
+ string3.toUnixSlash(relative4(this.#cwdPath, message.path)),
763
+ {
764
+ source: "hot-hook",
765
+ fullReload: false,
766
+ hotReloaded: true
767
+ },
768
+ this
769
+ );
770
+ }
771
+ }
772
+ });
773
+ this.#httpServer.then((result) => {
774
+ if (!this.#watcher) {
775
+ this.#onClose?.(result.exitCode);
776
+ } else {
777
+ this.ui.logger.info("Underlying HTTP server closed. Still watching for changes");
778
+ }
779
+ }).catch((error) => {
780
+ if (!this.#watcher) {
781
+ this.#onError?.(error);
782
+ } else {
783
+ this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
784
+ }
785
+ }).finally(() => {
786
+ resolve();
787
+ });
788
+ });
732
789
  }
733
790
  /**
734
791
  * Add listener to get notified when dev server is
@@ -747,11 +804,10 @@ var DevServer = class {
747
804
  return this;
748
805
  }
749
806
  /**
750
- * Close watchers and running child processes
807
+ * Close watchers and the running child process
751
808
  */
752
809
  async close() {
753
810
  await this.#watcher?.close();
754
- this.#assetsServer?.stop();
755
811
  if (this.#httpServer) {
756
812
  this.#httpServer.removeAllListeners();
757
813
  this.#httpServer.kill("SIGKILL");
@@ -760,173 +816,183 @@ var DevServer = class {
760
816
  /**
761
817
  * Start the development server
762
818
  */
763
- async start() {
764
- await this.#hooks.registerDevServerHooks();
819
+ async start(ts) {
820
+ const tsConfig = parseConfig(this.cwd, ts);
821
+ if (!tsConfig) {
822
+ return;
823
+ }
824
+ this.#stickyPort = String(await getPort(this.cwd));
825
+ this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
826
+ this.#hooks = await loadHooks(this.options.hooks, [
827
+ "devServerStarting",
828
+ "devServerStarted",
829
+ "fileAdded",
830
+ "fileChanged",
831
+ "fileRemoved"
832
+ ]);
833
+ this.#registerServerRestartHooks();
834
+ if (this.options.hmr) {
835
+ this.#mode = "hmr";
836
+ this.options.nodeArgs = this.options.nodeArgs.concat("--import=hot-hook/register");
837
+ this.options.env = {
838
+ ...this.options.env,
839
+ HOT_HOOK_INCLUDE: this.#fileSystem.includes.join(","),
840
+ HOT_HOOK_IGNORE: this.#fileSystem.excludes.join(","),
841
+ HOT_HOOK_RESTART: (this.options.metaFiles ?? []).filter(({ reloadServer }) => !!reloadServer).map(({ pattern }) => pattern).join(",")
842
+ };
843
+ }
765
844
  this.#clearScreen();
766
- this.#logger.info("starting HTTP server...");
767
- this.#startHTTPServer(String(await getPort(this.#cwd)), "nonblocking");
768
- this.#startAssetsServer();
845
+ this.ui.logger.info("starting HTTP server...");
846
+ await this.#startHTTPServer(this.#stickyPort);
769
847
  }
770
848
  /**
771
849
  * Start the development server in watch mode
772
850
  */
773
851
  async startAndWatch(ts, options) {
774
- await this.#hooks.registerDevServerHooks();
775
- const port = String(await getPort(this.#cwd));
776
- this.#isWatching = true;
777
- this.#clearScreen();
778
- this.#logger.info("starting HTTP server...");
779
- this.#startHTTPServer(port, "blocking");
780
- this.#startAssetsServer();
781
- const output = watch(this.#cwd, ts, options || {});
782
- if (!output) {
783
- this.#onClose?.(1);
852
+ const tsConfig = parseConfig(this.cwd, ts);
853
+ if (!tsConfig) {
784
854
  return;
785
855
  }
786
- this.#watcher = output.chokidar;
787
- output.watcher.on("watcher:ready", () => {
788
- this.#logger.info("watching file system for changes...");
856
+ this.#mode = "watch";
857
+ this.#stickyPort = String(await getPort(this.cwd));
858
+ this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
859
+ this.#hooks = await loadHooks(this.options.hooks, [
860
+ "devServerStarting",
861
+ "devServerStarted",
862
+ "fileAdded",
863
+ "fileChanged",
864
+ "fileRemoved"
865
+ ]);
866
+ this.#registerServerRestartHooks();
867
+ this.#clearScreen();
868
+ this.ui.logger.info("starting HTTP server...");
869
+ await this.#startHTTPServer(this.#stickyPort);
870
+ this.#watcher = watch({
871
+ usePolling: options?.poll ?? false,
872
+ cwd: this.#cwdPath,
873
+ ignoreInitial: true,
874
+ ignored: (file, stats) => {
875
+ if (!stats) {
876
+ return false;
877
+ }
878
+ if (stats.isFile()) {
879
+ return !this.#fileSystem.shouldWatchFile(file);
880
+ }
881
+ return !this.#fileSystem.shouldWatchDirectory(file);
882
+ }
883
+ });
884
+ this.#watcher.on("ready", () => {
885
+ this.ui.logger.info("watching file system for changes...");
789
886
  });
790
- output.chokidar.on("error", (error) => {
791
- this.#logger.warning("file system watcher failure");
792
- this.#logger.fatal(error);
887
+ this.#watcher.on("error", (error) => {
888
+ this.ui.logger.warning("file system watcher failure");
889
+ this.ui.logger.fatal(error);
793
890
  this.#onError?.(error);
794
- output.chokidar.close();
891
+ this.#watcher?.close();
795
892
  });
796
- output.watcher.on(
797
- "source:add",
798
- ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
799
- );
800
- output.watcher.on(
801
- "source:change",
802
- ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
803
- );
804
- output.watcher.on(
805
- "source:unlink",
806
- ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
807
- );
808
- output.watcher.on(
893
+ this.#watcher.on(
809
894
  "add",
810
- ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
895
+ (filePath) => this.#hooks.runner("fileAdded").run(string3.toUnixSlash(filePath), this)
811
896
  );
812
- output.watcher.on(
897
+ this.#watcher.on(
813
898
  "change",
814
- ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
899
+ (filePath) => this.#hooks.runner("fileChanged").run(
900
+ string3.toUnixSlash(filePath),
901
+ {
902
+ source: "watcher",
903
+ fullReload: true,
904
+ hotReloaded: false
905
+ },
906
+ this
907
+ )
815
908
  );
816
- output.watcher.on(
909
+ this.#watcher.on(
817
910
  "unlink",
818
- ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
911
+ (filePath) => this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(filePath), this)
819
912
  );
820
913
  }
821
914
  };
822
915
 
823
916
  // src/test_runner.ts
824
- import picomatch2 from "picomatch";
825
- import { cliui as cliui4 } from "@poppinss/cliui";
826
- var ui4 = cliui4();
917
+ import { cliui as cliui3 } from "@poppinss/cliui";
918
+ import { fileURLToPath as fileURLToPath5 } from "url";
919
+ import string4 from "@poppinss/utils/string";
827
920
  var TestRunner = class {
828
- #cwd;
829
- #logger = ui4.logger;
830
- #options;
831
- /**
832
- * The script file to run as a child process
833
- */
834
- #scriptFile = "bin/test.js";
921
+ constructor(cwd, options) {
922
+ this.cwd = cwd;
923
+ this.options = options;
924
+ }
835
925
  /**
836
- * Pico matcher function to check if the filepath is
837
- * part of the `metaFiles` glob patterns
926
+ * External listeners that are invoked when child process
927
+ * gets an error or closes
838
928
  */
839
- #isMetaFile;
929
+ #onError;
930
+ #onClose;
840
931
  /**
841
- * Pico matcher function to check if the filepath is
842
- * part of a test file.
932
+ * The stickyPort is set by the startAndWatch method and we will
933
+ * continue to use this port during re-runs
843
934
  */
844
- #isTestFile;
935
+ #stickyPort;
845
936
  /**
846
- * Arguments to pass to the "bin/test.js" file.
937
+ * Reference to chokidar watcher
847
938
  */
848
- #scriptArgs;
939
+ #watcher;
849
940
  /**
850
- * Set of initial filters applied when running the test
851
- * command. In watch mode, we will append an additional
852
- * filter to run tests only for the file that has been
853
- * changed.
941
+ * Reference to the test script child process
854
942
  */
855
- #initialFiltersArgs;
943
+ #testsProcess;
856
944
  /**
857
- * In watch mode, after a file is changed, we wait for the current
858
- * set of tests to finish before triggering a re-run. Therefore,
859
- * we use this flag to know if we are already busy in running
860
- * tests and ignore file-changes.
945
+ * Filesystem is used to decide which files to watch or entertain in watch
946
+ * mode
861
947
  */
862
- #isBusy = false;
948
+ #fileSystem;
863
949
  /**
864
- * External listeners that are invoked when child process
865
- * gets an error or closes
950
+ * Hooks to execute custom actions during the tests runner lifecycle
866
951
  */
867
- #onError;
868
- #onClose;
952
+ #hooks;
869
953
  /**
870
- * Reference to the test script child process
954
+ * Re-runs the test child process and throttle concurrent calls to
955
+ * ensure we do not end up with a long loop of restarts
871
956
  */
872
- #testScript;
957
+ #reRunTests = throttle(async (filters) => {
958
+ if (this.#testsProcess) {
959
+ this.#testsProcess.removeAllListeners();
960
+ this.#testsProcess.kill("SIGKILL");
961
+ }
962
+ await this.#runTests(this.#stickyPort, filters);
963
+ }, "reRunTests");
873
964
  /**
874
- * Reference to the watcher
965
+ * CLI UI to log colorful messages
875
966
  */
876
- #watcher;
967
+ ui = cliui3();
877
968
  /**
878
- * Reference to the assets server
969
+ * The script file to run as a child process
879
970
  */
880
- #assetsServer;
881
- /**
882
- * Getting reference to colors library from logger
883
- */
884
- get #colors() {
885
- return this.#logger.getColors();
886
- }
887
- constructor(cwd, options) {
888
- this.#cwd = cwd;
889
- this.#options = options;
890
- this.#isMetaFile = picomatch2((this.#options.metaFiles || []).map(({ pattern }) => pattern));
891
- this.#isTestFile = picomatch2(
892
- this.#options.suites.filter((suite) => {
893
- if (this.#options.filters.suites) {
894
- return this.#options.filters.suites.includes(suite.name);
895
- }
896
- return true;
897
- }).map((suite) => suite.files).flat(1)
898
- );
899
- this.#scriptArgs = this.#convertOptionsToArgs().concat(this.#options.scriptArgs);
900
- this.#initialFiltersArgs = this.#convertFiltersToArgs(this.#options.filters);
901
- }
971
+ scriptFile = "bin/test.ts";
902
972
  /**
903
973
  * Convert test runner options to the CLI args
904
974
  */
905
975
  #convertOptionsToArgs() {
906
976
  const args = [];
907
- if (this.#options.reporters) {
977
+ if (this.options.reporters) {
908
978
  args.push("--reporters");
909
- args.push(this.#options.reporters.join(","));
979
+ args.push(this.options.reporters.join(","));
910
980
  }
911
- if (this.#options.timeout !== void 0) {
981
+ if (this.options.timeout !== void 0) {
912
982
  args.push("--timeout");
913
- args.push(String(this.#options.timeout));
983
+ args.push(String(this.options.timeout));
914
984
  }
915
- if (this.#options.failed) {
985
+ if (this.options.failed) {
916
986
  args.push("--failed");
917
987
  }
918
- if (this.#options.retries !== void 0) {
988
+ if (this.options.retries !== void 0) {
919
989
  args.push("--retries");
920
- args.push(String(this.#options.retries));
990
+ args.push(String(this.options.retries));
921
991
  }
922
992
  return args;
923
993
  }
924
994
  /**
925
995
  * Converts all known filters to CLI args.
926
- *
927
- * The following code snippet may seem like repetitive code. But, it
928
- * is done intentionally to have visibility around how each filter
929
- * is converted to an arg.
930
996
  */
931
997
  #convertFiltersToArgs(filters) {
932
998
  const args = [];
@@ -955,94 +1021,74 @@ var TestRunner = class {
955
1021
  * Conditionally clear the terminal screen
956
1022
  */
957
1023
  #clearScreen() {
958
- if (this.#options.clearScreen) {
1024
+ if (this.options.clearScreen) {
959
1025
  process.stdout.write("\x1Bc");
960
1026
  }
961
1027
  }
962
1028
  /**
963
1029
  * Runs tests
964
1030
  */
965
- #runTests(port, mode, filters) {
966
- this.#isBusy = true;
967
- const scriptArgs = filters ? this.#convertFiltersToArgs(filters).concat(this.#scriptArgs) : this.#initialFiltersArgs.concat(this.#scriptArgs);
968
- this.#testScript = runNode(this.#cwd, {
969
- script: this.#scriptFile,
970
- reject: true,
971
- env: { PORT: port, ...this.#options.env },
972
- nodeArgs: this.#options.nodeArgs,
973
- scriptArgs
974
- });
975
- this.#testScript.then((result) => {
976
- if (mode === "nonblocking") {
977
- this.#onClose?.(result.exitCode);
978
- this.close();
979
- }
980
- }).catch((error) => {
981
- if (mode === "nonblocking") {
982
- this.#onError?.(error);
983
- this.close();
984
- }
985
- }).finally(() => {
986
- this.#isBusy = false;
1031
+ async #runTests(port, filters) {
1032
+ await this.#hooks.runner("testsStarting").run(this);
1033
+ debug_default('running tests using "%s" file, options %O', this.scriptFile, this.options);
1034
+ return new Promise(async (resolve) => {
1035
+ const scriptArgs = this.#convertOptionsToArgs().concat(this.options.scriptArgs).concat(
1036
+ this.#convertFiltersToArgs({
1037
+ ...this.options.filters,
1038
+ ...filters
1039
+ })
1040
+ );
1041
+ this.#testsProcess = runNode(this.cwd, {
1042
+ script: this.scriptFile,
1043
+ reject: true,
1044
+ env: { PORT: port, ...this.options.env },
1045
+ nodeArgs: this.options.nodeArgs,
1046
+ scriptArgs
1047
+ });
1048
+ this.#testsProcess.then((result) => {
1049
+ this.#hooks.runner("testsFinished").run(this).catch((error) => {
1050
+ this.ui.logger.error('One of the "testsFinished" hooks failed');
1051
+ this.ui.logger.fatal(error);
1052
+ }).finally(() => {
1053
+ if (!this.#watcher) {
1054
+ this.#onClose?.(result.exitCode);
1055
+ this.close();
1056
+ }
1057
+ });
1058
+ }).catch((error) => {
1059
+ if (!this.#watcher) {
1060
+ this.#onError?.(error);
1061
+ this.close();
1062
+ } else {
1063
+ this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
1064
+ }
1065
+ }).finally(() => resolve());
987
1066
  });
988
1067
  }
989
1068
  /**
990
- * Re-run tests with additional inline filters. Should be
991
- * executed in watch mode only.
1069
+ * Handles file change event
992
1070
  */
993
- #rerunTests(port, filters) {
994
- if (this.#testScript) {
995
- this.#testScript.removeAllListeners();
996
- this.#testScript.kill("SIGKILL");
997
- }
998
- this.#runTests(port, "blocking", filters);
999
- }
1000
- /**
1001
- * Starts the assets server
1002
- */
1003
- #startAssetsServer() {
1004
- this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
1005
- this.#assetsServer.setLogger(this.#logger);
1006
- this.#assetsServer.start();
1007
- }
1008
- /**
1009
- * Handles a non TypeScript file change
1010
- */
1011
- #handleFileChange(action, port, relativePath) {
1012
- if (this.#isBusy) {
1013
- return;
1014
- }
1015
- if (isDotEnvFile(relativePath) || this.#isMetaFile(relativePath)) {
1016
- this.#clearScreen();
1017
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
1018
- this.#rerunTests(port);
1019
- }
1020
- }
1021
- /**
1022
- * Handles TypeScript source file change
1023
- */
1024
- #handleSourceFileChange(action, port, relativePath) {
1025
- if (this.#isBusy) {
1071
+ #handleFileChange(filePath, action) {
1072
+ const file = this.#fileSystem.inspect(filePath);
1073
+ if (!file) {
1026
1074
  return;
1027
1075
  }
1028
1076
  this.#clearScreen();
1029
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
1030
- if (this.#isTestFile(relativePath)) {
1031
- this.#rerunTests(port, {
1032
- ...this.#options.filters,
1033
- files: [relativePath]
1034
- });
1035
- return;
1077
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
1078
+ if (file.fileType === "test") {
1079
+ this.#reRunTests({ files: [filePath] });
1080
+ } else {
1081
+ this.#reRunTests();
1036
1082
  }
1037
- this.#rerunTests(port);
1038
1083
  }
1039
1084
  /**
1040
- * Set a custom CLI UI logger
1085
+ * Registers inline hooks for the file changes and restarts the
1086
+ * HTTP server when a file gets changed.
1041
1087
  */
1042
- setLogger(logger) {
1043
- this.#logger = logger;
1044
- this.#assetsServer?.setLogger(logger);
1045
- return this;
1088
+ #registerServerRestartHooks() {
1089
+ this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
1090
+ this.#hooks.add("fileChanged", (filePath) => this.#handleFileChange(filePath, "update"));
1091
+ this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
1046
1092
  }
1047
1093
  /**
1048
1094
  * Add listener to get notified when dev server is
@@ -1065,69 +1111,98 @@ var TestRunner = class {
1065
1111
  */
1066
1112
  async close() {
1067
1113
  await this.#watcher?.close();
1068
- this.#assetsServer?.stop();
1069
- if (this.#testScript) {
1070
- this.#testScript.removeAllListeners();
1071
- this.#testScript.kill("SIGKILL");
1114
+ if (this.#testsProcess) {
1115
+ this.#testsProcess.removeAllListeners();
1116
+ this.#testsProcess.kill("SIGKILL");
1072
1117
  }
1073
1118
  }
1074
1119
  /**
1075
1120
  * Runs tests
1076
1121
  */
1077
1122
  async run() {
1078
- const port = String(await getPort(this.#cwd));
1123
+ this.#stickyPort = String(await getPort(this.cwd));
1124
+ this.#hooks = await loadHooks(this.options.hooks, [
1125
+ "testsStarting",
1126
+ "testsFinished",
1127
+ "fileAdded",
1128
+ "fileChanged",
1129
+ "fileRemoved"
1130
+ ]);
1079
1131
  this.#clearScreen();
1080
- this.#startAssetsServer();
1081
- this.#logger.info("booting application to run tests...");
1082
- this.#runTests(port, "nonblocking");
1132
+ this.ui.logger.info("booting application to run tests...");
1133
+ await this.#runTests(this.#stickyPort);
1083
1134
  }
1084
1135
  /**
1085
1136
  * Run tests in watch mode
1086
1137
  */
1087
1138
  async runAndWatch(ts, options) {
1088
- const port = String(await getPort(this.#cwd));
1089
- this.#clearScreen();
1090
- this.#startAssetsServer();
1091
- this.#logger.info("booting application to run tests...");
1092
- this.#runTests(port, "blocking");
1093
- const output = watch(this.#cwd, ts, options || {});
1094
- if (!output) {
1095
- this.#onClose?.(1);
1139
+ const tsConfig = parseConfig(this.cwd, ts);
1140
+ if (!tsConfig) {
1096
1141
  return;
1097
1142
  }
1098
- this.#watcher = output.chokidar;
1099
- output.watcher.on("watcher:ready", () => {
1100
- this.#logger.info("watching file system for changes...");
1143
+ this.#stickyPort = String(await getPort(this.cwd));
1144
+ this.#fileSystem = new FileSystem(this.cwd, tsConfig, {
1145
+ ...this.options,
1146
+ suites: this.options.suites?.filter((suite) => {
1147
+ if (this.options.filters.suites) {
1148
+ return this.options.filters.suites.includes(suite.name);
1149
+ }
1150
+ return true;
1151
+ })
1101
1152
  });
1102
- output.chokidar.on("error", (error) => {
1103
- this.#logger.warning("file system watcher failure");
1104
- this.#logger.fatal(error);
1153
+ this.#hooks = await loadHooks(this.options.hooks, [
1154
+ "testsStarting",
1155
+ "testsFinished",
1156
+ "fileAdded",
1157
+ "fileChanged",
1158
+ "fileRemoved"
1159
+ ]);
1160
+ this.#registerServerRestartHooks();
1161
+ this.#clearScreen();
1162
+ this.ui.logger.info("booting application to run tests...");
1163
+ await this.#runTests(this.#stickyPort);
1164
+ this.#watcher = watch({
1165
+ usePolling: options?.poll ?? false,
1166
+ cwd: fileURLToPath5(this.cwd),
1167
+ ignoreInitial: true,
1168
+ ignored: (file, stats) => {
1169
+ if (!stats) {
1170
+ return false;
1171
+ }
1172
+ if (stats.isFile()) {
1173
+ return !this.#fileSystem.shouldWatchFile(file);
1174
+ }
1175
+ return !this.#fileSystem.shouldWatchDirectory(file);
1176
+ }
1177
+ });
1178
+ this.#watcher.on("ready", () => {
1179
+ this.ui.logger.info("watching file system for changes...");
1180
+ });
1181
+ this.#watcher.on("error", (error) => {
1182
+ this.ui.logger.warning("file system watcher failure");
1183
+ this.ui.logger.fatal(error);
1105
1184
  this.#onError?.(error);
1106
- output.chokidar.close();
1185
+ this.#watcher?.close();
1107
1186
  });
1108
- output.watcher.on(
1109
- "source:add",
1110
- ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
1111
- );
1112
- output.watcher.on(
1113
- "source:change",
1114
- ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
1115
- );
1116
- output.watcher.on(
1117
- "source:unlink",
1118
- ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
1119
- );
1120
- output.watcher.on(
1187
+ this.#watcher.on(
1121
1188
  "add",
1122
- ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
1189
+ (filePath) => this.#hooks.runner("fileAdded").run(string4.toUnixSlash(filePath), this)
1123
1190
  );
1124
- output.watcher.on(
1191
+ this.#watcher.on(
1125
1192
  "change",
1126
- ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
1193
+ (filePath) => this.#hooks.runner("fileChanged").run(
1194
+ string4.toUnixSlash(filePath),
1195
+ {
1196
+ source: "watcher",
1197
+ fullReload: true,
1198
+ hotReloaded: false
1199
+ },
1200
+ this
1201
+ )
1127
1202
  );
1128
- output.watcher.on(
1203
+ this.#watcher.on(
1129
1204
  "unlink",
1130
- ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
1205
+ (filePath) => this.#hooks.runner("fileRemoved").run(string4.toUnixSlash(filePath), this)
1131
1206
  );
1132
1207
  }
1133
1208
  };
@@ -1136,4 +1211,3 @@ export {
1136
1211
  DevServer,
1137
1212
  TestRunner
1138
1213
  };
1139
- //# sourceMappingURL=index.js.map