@adonisjs/assembler 7.8.1 → 8.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,32 +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
- script: assetsBundler.cmd,
281
- scriptArgs: assetsBundler.args
282
- });
283
- return true;
284
- } catch {
285
- return false;
286
- }
287
- }
288
230
  /**
289
231
  * Runs tsc command to build the source.
290
232
  */
291
233
  async #runTsc(outDir) {
292
234
  try {
293
- await run(this.#cwd, {
235
+ await run(this.cwd, {
294
236
  stdio: "inherit",
295
237
  script: "tsc",
296
238
  scriptArgs: ["--outDir", outDir]
@@ -304,26 +246,21 @@ var Bundler = class {
304
246
  * Copy meta files to the output directory
305
247
  */
306
248
  async #copyMetaFiles(outDir, additionalFilesToCopy) {
307
- const metaFiles = (this.#options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy);
249
+ const metaFiles = (this.options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy);
308
250
  await copyFiles(metaFiles, this.#cwdPath, outDir);
309
251
  }
310
252
  /**
311
253
  * Detect the package manager used by the project
312
- * and return the lockfile name and install command
313
- * related to it.
314
254
  */
315
- async #getPackageManager(client) {
316
- let pkgManager = client;
317
- if (!pkgManager) {
318
- pkgManager = await detectPackageManager(this.#cwdPath);
255
+ async #detectPackageManager() {
256
+ const pkgManager = await detectPackageManager(this.#cwdPath);
257
+ if (pkgManager === "deno") {
258
+ return "npm";
319
259
  }
320
- if (!pkgManager) {
321
- pkgManager = "npm";
260
+ if (pkgManager === "pnpm@6") {
261
+ return "pnpm";
322
262
  }
323
- if (!Object.keys(SUPPORT_PACKAGE_MANAGERS).includes(pkgManager)) {
324
- return null;
325
- }
326
- return SUPPORT_PACKAGE_MANAGERS[pkgManager];
263
+ return pkgManager;
327
264
  }
328
265
  /**
329
266
  * Rewrite the ace file since the original one
@@ -345,257 +282,346 @@ var Bundler = class {
345
282
  `
346
283
  );
347
284
  await fs.writeFile(aceFileLocation, aceFileContent);
348
- this.#logger.info("rewrited ace file", { suffix: this.#getRelativeName(aceFileLocation) });
349
- }
350
- /**
351
- * Set a custom CLI UI logger
352
- */
353
- setLogger(logger) {
354
- this.#logger = logger;
355
- return this;
285
+ this.ui.logger.info("created ace file", { suffix: this.#getRelativeName(aceFileLocation) });
356
286
  }
357
287
  /**
358
288
  * Bundles the application to be run in production
359
289
  */
360
290
  async bundle(stopOnError = true, client) {
361
- await this.#hooks.registerBuildHooks();
362
- 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);
363
294
  if (!config) {
364
295
  return false;
365
296
  }
366
- const outDir = config.options.outDir || fileURLToPath2(new URL("build/", this.#cwd));
367
- 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) });
368
299
  await this.#cleanupBuildDirectory(outDir);
369
- if (!await this.#buildAssets()) {
370
- return false;
371
- }
372
- await this.#hooks.onBuildStarting({ colors: ui.colors, logger: this.#logger });
373
- 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" });
374
302
  const buildCompleted = await this.#runTsc(outDir);
375
303
  await this.#createAceFile(outDir);
376
304
  if (!buildCompleted && stopOnError) {
377
305
  await this.#cleanupBuildDirectory(outDir);
378
- const instructions = ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
306
+ const instructions = this.ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
379
307
  instructions.add(
380
- 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.")
381
309
  );
382
310
  instructions.add(
383
- this.#colors.red(
311
+ this.ui.colors.red(
384
312
  'Use "--ignore-ts-errors" flag to ignore TypeScript errors and continue the build.'
385
313
  )
386
314
  );
387
- this.#logger.logError(instructions.prepare());
315
+ this.ui.logger.logError(instructions.prepare());
388
316
  return false;
389
317
  }
390
- const pkgManager = await this.#getPackageManager(client);
391
- const pkgFiles = pkgManager ? ["package.json", ...pkgManager.packageManagerFiles] : ["package.json"];
392
- 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");
393
323
  await this.#copyMetaFiles(outDir, pkgFiles);
394
- this.#logger.success("build completed");
395
- this.#logger.log("");
396
- await this.#hooks.onBuildCompleted({ colors: ui.colors, logger: this.#logger });
397
- 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(
398
- this.#colors.cyan(
399
- pkgManager ? pkgManager.installCommand : "Install production dependencies"
400
- )
401
- ).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();
402
329
  return true;
403
330
  }
404
331
  };
405
332
 
406
333
  // src/dev_server.ts
407
- import picomatch from "picomatch";
408
- import { relative as relative3 } from "node:path";
334
+ import { cliui as cliui2 } from "@poppinss/cliui";
409
335
  import prettyHrtime from "pretty-hrtime";
410
- import { fileURLToPath as fileURLToPath3 } from "node:url";
411
- import { cliui as cliui3 } from "@poppinss/cliui";
336
+ import { fileURLToPath as fileURLToPath4 } from "url";
337
+ import string3 from "@poppinss/utils/string";
412
338
 
413
- // src/assets_dev_server.ts
414
- import { cliui as cliui2 } from "@poppinss/cliui";
415
- var ui2 = cliui2();
416
- var AssetsDevServer = class {
339
+ // src/file_system.ts
340
+ import picomatch from "picomatch";
341
+ import { fileURLToPath as fileURLToPath3 } from "url";
342
+ import { join as join3, relative as relative3 } from "path";
343
+ import string2 from "@poppinss/utils/string";
344
+ var DEFAULT_INCLUDES = ["**/*"];
345
+ var ALWAYS_EXCLUDE = [".git/**", "coverage/**", ".github/**"];
346
+ var DEFAULT_EXCLUDES = ["node_modules/**", "bower_components/**", "jspm_packages/**"];
347
+ var FileSystem = class {
348
+ /**
349
+ * The current working project directory
350
+ */
417
351
  #cwd;
418
- #logger = ui2.logger;
419
- #options;
420
- #devServer;
421
352
  /**
422
- * Getting reference to colors library from logger
353
+ * Referenced to the parsed ts config file. We use it to read the includes,
354
+ * excludes and pre-scanned files.
423
355
  */
424
- get #colors() {
425
- return this.#logger.getColors();
426
- }
427
- constructor(cwd, options) {
428
- this.#cwd = cwd;
429
- this.#options = options;
430
- }
356
+ #tsConfig;
431
357
  /**
432
- * Logs messages from vite dev server stdout and stderr
358
+ * Set of pre-scanned typeScript files provided by tsconfig
433
359
  */
434
- #logViteDevServerMessage(data) {
435
- const dataString = data.toString();
436
- const lines = dataString.split("\n");
437
- if (dataString.includes("Local") && dataString.includes("Network")) {
438
- const sticker = ui2.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer());
439
- lines.forEach((line) => {
440
- if (line.trim()) {
441
- sticker.add(line);
442
- }
443
- });
444
- sticker.render();
445
- return;
446
- }
447
- if (dataString.includes("ready in")) {
448
- console.log("");
449
- console.log(dataString.trim());
450
- return;
451
- }
452
- lines.forEach((line) => {
453
- if (line.trim()) {
454
- console.log(line);
455
- }
456
- });
457
- }
360
+ #scannedTypeScriptFiles = /* @__PURE__ */ new Set();
458
361
  /**
459
- * Logs messages from assets dev server stdout and stderr
362
+ * Picomatch matcher function to know if a file path is
363
+ * part of includes
460
364
  */
461
- #logAssetsDevServerMessage(data) {
462
- const dataString = data.toString();
463
- const lines = dataString.split("\n");
464
- lines.forEach((line) => {
465
- if (line.trim()) {
466
- console.log(line);
467
- }
468
- });
469
- }
365
+ #isIncluded;
470
366
  /**
471
- * Set a custom CLI UI logger
367
+ * Picomatch matcher function to know if a file path is
368
+ * part of excludes
472
369
  */
473
- setLogger(logger) {
474
- this.#logger = logger;
475
- return this;
370
+ #isExcluded;
371
+ /**
372
+ * Picomatch matcher function to know if a file path is a
373
+ * meta file with reloadServer option enabled
374
+ */
375
+ #isMetaFileWithReloadsEnabled;
376
+ /**
377
+ * Picomatch matcher function to know if a file path is a
378
+ * meta file with reloadServer option disabled
379
+ */
380
+ #isMetaFileWithReloadsDisabled;
381
+ /**
382
+ * Picomatch matcher function to know if a file path is a
383
+ * test file or not
384
+ */
385
+ #isTestFile;
386
+ /**
387
+ * References to includes and excludes
388
+ */
389
+ #includes;
390
+ #excludes;
391
+ /**
392
+ * Includes glob patterns extracted from "tsconfig.json" file.
393
+ * Defaults to: ["**\/*"]
394
+ */
395
+ get includes() {
396
+ return this.#includes;
476
397
  }
477
398
  /**
478
- * Starts the assets bundler server. The assets bundler server process is
479
- * considered as the secondary process and therefore we do not perform
480
- * any cleanup if it dies.
399
+ * Excludes glob patterns extracted from "tsconfig.json" file.
400
+ *
401
+ * Defaults to: [
402
+ * 'node_modules/**',
403
+ * 'bower_components/**',
404
+ * 'jspm_packages/**,
405
+ * ]
406
+ *
407
+ * Following patterns are always ignored
408
+ *
409
+ * '.git/**', 'coverage/**', '.github/**'
410
+ */
411
+ get excludes() {
412
+ return this.#excludes;
413
+ }
414
+ /**
415
+ * Inspect a relative path to find its source in the project
416
+ */
417
+ inspect = memoize((filePath) => {
418
+ const relativePath = filePath;
419
+ const absolutePath = string2.toUnixSlash(join3(this.#cwd, relativePath));
420
+ if (this.#isScriptFile(relativePath) && (this.#scannedTypeScriptFiles.has(absolutePath) || this.#isPartOfBackendProject(relativePath))) {
421
+ debug_default('backend project file "%s"', relativePath);
422
+ const isTestFile = this.#isTestFile(relativePath);
423
+ return {
424
+ fileType: isTestFile ? "test" : "script",
425
+ reloadServer: !isTestFile,
426
+ unixRelativePath: relativePath,
427
+ unixAbsolutePath: absolutePath
428
+ };
429
+ }
430
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
431
+ debug_default('meta file "%s"', relativePath);
432
+ return {
433
+ fileType: "meta",
434
+ reloadServer: true,
435
+ unixRelativePath: relativePath,
436
+ unixAbsolutePath: absolutePath
437
+ };
438
+ }
439
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
440
+ debug_default('meta file "%s"', relativePath);
441
+ return {
442
+ fileType: "meta",
443
+ reloadServer: false,
444
+ unixRelativePath: relativePath,
445
+ unixAbsolutePath: absolutePath
446
+ };
447
+ }
448
+ debug_default('ignored file "%s"', relativePath);
449
+ return null;
450
+ });
451
+ /**
452
+ * Returns true if the directory should be watched. Chokidar sends
453
+ * absolute unix paths to the ignored callback.
454
+ *
455
+ * You must use "shouldWatchFile" method for files and call this method
456
+ * for directories only.
481
457
  */
482
- start() {
483
- if (!this.#options?.enabled) {
484
- return;
458
+ shouldWatchDirectory = memoize((absolutePath) => {
459
+ if (absolutePath === this.#cwd) {
460
+ debug_default("watching project root");
461
+ return true;
485
462
  }
486
- this.#logger.info(`starting "${this.#options.driver}" dev server...`);
487
- this.#devServer = run(this.#cwd, {
488
- script: this.#options.cmd,
489
- /**
490
- * We do not inherit the stdio for vite and encore, because in
491
- * inherit mode they own the stdin and interrupts the
492
- * `Ctrl + C` command.
493
- */
494
- stdio: "pipe",
495
- scriptArgs: this.#options.args
496
- });
497
- this.#devServer.stdout?.on("data", (data) => {
498
- if (this.#options.driver === "vite") {
499
- this.#logViteDevServerMessage(data);
500
- } else {
501
- this.#logAssetsDevServerMessage(data);
463
+ const relativePath = string2.toUnixSlash(relative3(this.#cwd, absolutePath));
464
+ if (this.#isExcluded(relativePath)) {
465
+ debug_default('watching "%s"', absolutePath);
466
+ return false;
467
+ }
468
+ return true;
469
+ });
470
+ constructor(cwd, tsConfig, rcFile) {
471
+ this.#cwd = string2.toUnixSlash(typeof cwd === "string" ? cwd : fileURLToPath3(cwd));
472
+ this.#tsConfig = tsConfig;
473
+ const files = tsConfig.fileNames;
474
+ const metaFiles = rcFile.metaFiles ?? [];
475
+ const testSuites = rcFile.suites ?? [];
476
+ const outDir = tsConfig.raw.compilerOptions?.outDir;
477
+ files.forEach((file) => this.#scannedTypeScriptFiles.add(string2.toUnixSlash(file)));
478
+ this.#includes = tsConfig.raw.include || DEFAULT_INCLUDES;
479
+ this.#excludes = ALWAYS_EXCLUDE.concat(
480
+ tsConfig.raw.exclude || (outDir ? DEFAULT_EXCLUDES.concat(outDir) : DEFAULT_EXCLUDES)
481
+ );
482
+ this.#isMetaFileWithReloadsEnabled = picomatch(
483
+ metaFiles.filter((file) => !!file.reloadServer).map((file) => file.pattern),
484
+ {
485
+ cwd: this.#cwd
502
486
  }
503
- });
504
- this.#devServer.stderr?.on("data", (data) => {
505
- if (this.#options.driver === "vite") {
506
- this.#logViteDevServerMessage(data);
507
- } else {
508
- this.#logAssetsDevServerMessage(data);
487
+ );
488
+ this.#isMetaFileWithReloadsDisabled = picomatch(
489
+ metaFiles.filter((file) => !file.reloadServer).map((file) => file.pattern),
490
+ {
491
+ cwd: this.#cwd
509
492
  }
493
+ );
494
+ this.#isTestFile = picomatch(
495
+ testSuites.flatMap((suite) => suite.files),
496
+ {
497
+ cwd: this.#cwd
498
+ }
499
+ );
500
+ this.#isIncluded = picomatch(this.#includes, {
501
+ cwd: this.#cwd
510
502
  });
511
- this.#devServer.then((result) => {
512
- this.#logger.warning(
513
- `"${this.#options.driver}" dev server closed with status code "${result.exitCode}"`
514
- );
515
- }).catch((error) => {
516
- this.#logger.warning(`unable to connect to "${this.#options.driver}" dev server`);
517
- this.#logger.fatal(error);
503
+ this.#isExcluded = picomatch(this.#excludes, {
504
+ cwd: this.#cwd
505
+ });
506
+ debug_default("initiating file system %O", {
507
+ includes: this.#includes,
508
+ excludes: this.#includes,
509
+ outDir,
510
+ files,
511
+ metaFiles,
512
+ testSuites
518
513
  });
519
514
  }
520
515
  /**
521
- * Stop the dev server
516
+ * Returns a boolean telling if a file path is a script file or not.
517
+ *
518
+ * - Files ending with ".ts", ".tsx" are considered are script files.
519
+ * - Files ending with ".js" with "allowJs" option enabled are considered are script files.
520
+ * - Files ending with ".json" with "resolveJsonModule" option enabled are considered are script files.
522
521
  */
523
- stop() {
524
- if (this.#devServer) {
525
- this.#devServer.removeAllListeners();
526
- this.#devServer.kill("SIGKILL");
527
- this.#devServer = void 0;
522
+ #isScriptFile(relativePath) {
523
+ if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
524
+ return true;
525
+ }
526
+ if (this.#tsConfig.options.allowJs && relativePath.endsWith(".js")) {
527
+ return true;
528
528
  }
529
+ if (this.#tsConfig.options.resolveJsonModule && relativePath.endsWith(".json")) {
530
+ return true;
531
+ }
532
+ return false;
533
+ }
534
+ /**
535
+ * Check if the file path is part of the backend TypeScript project. We use
536
+ * tsconfig "includes", "excludes", and "files" paths to test if the file
537
+ * should be considered or not.
538
+ */
539
+ #isPartOfBackendProject(relativePath) {
540
+ if (this.#isExcluded(relativePath)) {
541
+ debug_default('excluded by tsconfig "%s"', relativePath);
542
+ return false;
543
+ }
544
+ if (this.#isIncluded(relativePath)) {
545
+ debug_default('included by tsconfig "%s"', relativePath);
546
+ return true;
547
+ }
548
+ return false;
549
+ }
550
+ /**
551
+ * Returns true if the file should be watched. Chokidar sends
552
+ * absolute unix paths to the ignored callback.
553
+ *
554
+ * You must use "shouldWatchDirectory" method for directories and call
555
+ * this method for files only.
556
+ */
557
+ shouldWatchFile(absolutePath) {
558
+ return this.inspect(relative3(this.#cwd, absolutePath)) !== null;
529
559
  }
530
560
  };
531
561
 
532
562
  // src/dev_server.ts
533
- var ui3 = cliui3();
534
563
  var DevServer = class {
535
- #cwd;
536
- #logger = ui3.logger;
537
- #options;
538
- /**
539
- * Flag to know if the dev server is running in watch
540
- * mode
541
- */
542
- #isWatching = false;
564
+ constructor(cwd, options) {
565
+ this.cwd = cwd;
566
+ this.options = options;
567
+ }
543
568
  /**
544
- * Script file to start the development server
569
+ * External listeners that are invoked when child process
570
+ * gets an error or closes
545
571
  */
546
- #scriptFile = "bin/server.js";
572
+ #onError;
573
+ #onClose;
547
574
  /**
548
- * Picomatch matcher function to know if a file path is a
549
- * meta file with reloadServer option enabled
575
+ * The stickyPort is set by the start and the startAndWatch methods
576
+ * and we will continue to use that port during restart
550
577
  */
551
- #isMetaFileWithReloadsEnabled;
578
+ #stickyPort;
552
579
  /**
553
- * Picomatch matcher function to know if a file path is a
554
- * meta file with reloadServer option disabled
580
+ * The mode is set by the start and the startAndWatch methods
555
581
  */
556
- #isMetaFileWithReloadsDisabled;
582
+ #mode = "static";
557
583
  /**
558
- * External listeners that are invoked when child process
559
- * gets an error or closes
584
+ * Reference to chokidar watcher
560
585
  */
561
- #onError;
562
- #onClose;
586
+ #watcher;
563
587
  /**
564
588
  * Reference to the child process
565
589
  */
566
590
  #httpServer;
567
591
  /**
568
- * Reference to the watcher
592
+ * Filesystem is used to decide which files to watch or entertain when
593
+ * using hot-hook
569
594
  */
570
- #watcher;
571
- /**
572
- * Reference to the assets server
573
- */
574
- #assetsServer;
595
+ #fileSystem;
575
596
  /**
576
597
  * Hooks to execute custom actions during the dev server lifecycle
577
598
  */
578
599
  #hooks;
579
600
  /**
580
- * Getting reference to colors library from logger
601
+ * Restarts the HTTP server and throttle concurrent calls to
602
+ * ensure we do not end up with a long loop of restarts
581
603
  */
582
- get #colors() {
583
- return this.#logger.getColors();
584
- }
585
- constructor(cwd, options) {
586
- this.#cwd = cwd;
587
- this.#options = options;
588
- this.#hooks = new AssemblerHooks(options.hooks);
589
- if (this.#options.hmr) {
590
- this.#options.nodeArgs = this.#options.nodeArgs.concat(["--import=hot-hook/register"]);
604
+ #restartHTTPServer = throttle(async () => {
605
+ if (this.#httpServer) {
606
+ this.#httpServer.removeAllListeners();
607
+ this.#httpServer.kill("SIGKILL");
591
608
  }
592
- this.#isMetaFileWithReloadsEnabled = picomatch(
593
- (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer === true).map(({ pattern }) => pattern)
594
- );
595
- this.#isMetaFileWithReloadsDisabled = picomatch(
596
- (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer !== true).map(({ pattern }) => pattern)
597
- );
609
+ await this.#startHTTPServer(this.#stickyPort);
610
+ }, "restartHTTPServer");
611
+ /**
612
+ * CLI UI to log colorful messages
613
+ */
614
+ ui = cliui2();
615
+ /**
616
+ * The mode in which the DevServer is running.
617
+ */
618
+ get mode() {
619
+ return this.#mode;
598
620
  }
621
+ /**
622
+ * Script file to start the development server
623
+ */
624
+ scriptFile = "bin/server.ts";
599
625
  /**
600
626
  * Inspect if child process message is from AdonisJS HTTP server
601
627
  */
@@ -603,7 +629,25 @@ var DevServer = class {
603
629
  return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
604
630
  }
605
631
  /**
606
- * Inspect if child process message is coming from Hot Hook
632
+ * Displays the server info and executes the hooks after the server has been
633
+ * started.
634
+ */
635
+ async #postServerReady(message) {
636
+ const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
637
+ const displayMessage = this.ui.sticker().add(`Server address: ${this.ui.colors.cyan(`http://${host}:${message.port}`)}`).add(`Mode: ${this.ui.colors.cyan(this.mode)}`);
638
+ if (message.duration) {
639
+ displayMessage.add(`Ready in: ${this.ui.colors.cyan(prettyHrtime(message.duration))}`);
640
+ }
641
+ try {
642
+ await this.#hooks.runner("devServerStarted").run(this, displayMessage);
643
+ } catch (error) {
644
+ this.ui.logger.error('One of the "devServerStarted" hooks failed');
645
+ this.ui.logger.fatal(error);
646
+ }
647
+ displayMessage.render();
648
+ }
649
+ /**
650
+ * Inspect if child process message is coming from hot-hook
607
651
  */
608
652
  #isHotHookMessage(message) {
609
653
  return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
@@ -612,120 +656,96 @@ var DevServer = class {
612
656
  * Conditionally clear the terminal screen
613
657
  */
614
658
  #clearScreen() {
615
- if (this.#options.clearScreen) {
659
+ if (this.options.clearScreen) {
616
660
  process.stdout.write("\x1Bc");
617
661
  }
618
662
  }
619
663
  /**
620
- * Starts the HTTP server
621
- */
622
- #startHTTPServer(port, mode) {
623
- const hooksArgs = { colors: this.#colors, logger: this.#logger };
624
- this.#httpServer = runNode(this.#cwd, {
625
- script: this.#scriptFile,
626
- env: { PORT: port, ...this.#options.env },
627
- nodeArgs: this.#options.nodeArgs,
628
- scriptArgs: this.#options.scriptArgs
629
- });
630
- this.#httpServer.on("message", async (message) => {
631
- if (this.#isHotHookMessage(message)) {
632
- const path = relative3(fileURLToPath3(this.#cwd), message.path || message.paths?.[0]);
633
- this.#hooks.onSourceFileChanged(hooksArgs, path);
634
- if (message.type === "hot-hook:full-reload") {
635
- this.#clearScreen();
636
- this.#logger.log(`${this.#colors.green("full-reload")} ${path}`);
637
- this.#restartHTTPServer(port);
638
- this.#hooks.onDevServerStarted(hooksArgs);
639
- } else if (message.type === "hot-hook:invalidated") {
640
- this.#logger.log(`${this.#colors.green("invalidated")} ${path}`);
641
- }
642
- }
643
- if (this.#isAdonisJSReadyMessage(message)) {
644
- const host = message.host === "0.0.0.0" ? "127.0.0.1" : message.host;
645
- const displayMessage = ui3.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer()).add(`Server address: ${this.#colors.cyan(`http://${host}:${message.port}`)}`);
646
- const watchMode = this.#options.hmr ? "HMR" : this.#isWatching ? "Legacy" : "None";
647
- displayMessage.add(`Watch Mode: ${this.#colors.cyan(watchMode)}`);
648
- if (message.duration) {
649
- displayMessage.add(`Ready in: ${this.#colors.cyan(prettyHrtime(message.duration))}`);
650
- }
651
- displayMessage.render();
652
- await this.#hooks.onDevServerStarted({ colors: ui3.colors, logger: this.#logger });
653
- }
654
- });
655
- this.#httpServer.then((result) => {
656
- if (mode === "nonblocking") {
657
- this.#onClose?.(result.exitCode);
658
- this.#watcher?.close();
659
- this.#assetsServer?.stop();
660
- } else {
661
- this.#logger.info("Underlying HTTP server closed. Still watching for changes");
662
- }
663
- }).catch((error) => {
664
- if (mode === "nonblocking") {
665
- this.#onError?.(error);
666
- this.#watcher?.close();
667
- this.#assetsServer?.stop();
668
- } else {
669
- this.#logger.info("Underlying HTTP server died. Still watching for changes");
670
- }
671
- });
672
- }
673
- /**
674
- * Starts the assets server
675
- */
676
- #startAssetsServer() {
677
- this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
678
- this.#assetsServer.setLogger(this.#logger);
679
- this.#assetsServer.start();
680
- }
681
- /**
682
- * Restarts the HTTP server in the watch mode. Do not call this
683
- * method when not in watch mode
684
- */
685
- #restartHTTPServer(port) {
686
- if (this.#httpServer) {
687
- this.#httpServer.removeAllListeners();
688
- this.#httpServer.kill("SIGKILL");
689
- }
690
- this.#startHTTPServer(port, "blocking");
691
- }
692
- /**
693
- * Handles a non TypeScript file change
664
+ * Handles file change event
694
665
  */
695
- #handleFileChange(action, port, relativePath) {
696
- if (isDotEnvFile(relativePath)) {
697
- this.#clearScreen();
698
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
699
- this.#restartHTTPServer(port);
666
+ #handleFileChange(filePath, action, hotReplaced) {
667
+ const file = this.#fileSystem.inspect(filePath);
668
+ if (!file) {
700
669
  return;
701
670
  }
702
- if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
703
- this.#clearScreen();
704
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
705
- this.#restartHTTPServer(port);
671
+ if (hotReplaced) {
672
+ this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${filePath}`);
706
673
  return;
707
674
  }
708
- if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
675
+ if (file.reloadServer) {
709
676
  this.#clearScreen();
710
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
677
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
678
+ this.#restartHTTPServer();
679
+ return;
711
680
  }
681
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
712
682
  }
713
683
  /**
714
- * Handles TypeScript source file change
684
+ * Registers inline hooks for the file changes and restarts the
685
+ * HTTP server when a file gets changed.
715
686
  */
716
- async #handleSourceFileChange(action, port, relativePath) {
717
- await this.#hooks.onSourceFileChanged({ colors: ui3.colors, logger: this.#logger }, relativePath);
718
- this.#clearScreen();
719
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
720
- this.#restartHTTPServer(port);
687
+ #registerServerRestartHooks() {
688
+ this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
689
+ this.#hooks.add(
690
+ "fileChanged",
691
+ (filePath, hotReplaced) => this.#handleFileChange(filePath, "update", hotReplaced)
692
+ );
693
+ this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
721
694
  }
722
695
  /**
723
- * Set a custom CLI UI logger
696
+ * Starts the HTTP server
724
697
  */
725
- setLogger(logger) {
726
- this.#logger = logger;
727
- this.#assetsServer?.setLogger(logger);
728
- return this;
698
+ async #startHTTPServer(port) {
699
+ await this.#hooks.runner("devServerStarting").run(this);
700
+ debug_default('starting http server using "%s" file, options %O', this.scriptFile, this.options);
701
+ return new Promise(async (resolve) => {
702
+ this.#httpServer = runNode(this.cwd, {
703
+ script: this.scriptFile,
704
+ env: { PORT: port, ...this.options.env },
705
+ nodeArgs: this.options.nodeArgs,
706
+ reject: true,
707
+ scriptArgs: this.options.scriptArgs
708
+ });
709
+ this.#httpServer.on("message", async (message) => {
710
+ if (this.#isAdonisJSReadyMessage(message)) {
711
+ await this.#postServerReady(message);
712
+ resolve();
713
+ } else if (this.#mode === "hmr" && this.#isHotHookMessage(message)) {
714
+ if (message.type === "hot-hook:file-changed") {
715
+ switch (message.action) {
716
+ case "add":
717
+ this.#hooks.runner("fileAdded").run(string3.toUnixSlash(message.path), this);
718
+ break;
719
+ case "change":
720
+ this.#hooks.runner("fileChanged").run(string3.toUnixSlash(message.path), false, this);
721
+ break;
722
+ case "unlink":
723
+ this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(message.path), this);
724
+ }
725
+ } else if (message.type === "hot-hook:full-reload") {
726
+ this.#hooks.runner("fileChanged").run(string3.toUnixSlash(message.path), false, this);
727
+ } else if (message.type === "hot-hook:invalidated") {
728
+ this.#hooks.runner("fileChanged").run(string3.toUnixSlash(message.path), true, this);
729
+ }
730
+ }
731
+ });
732
+ this.#httpServer.then((result) => {
733
+ if (!this.#watcher) {
734
+ this.#onClose?.(result.exitCode);
735
+ } else {
736
+ this.ui.logger.info("Underlying HTTP server closed. Still watching for changes");
737
+ }
738
+ }).catch((error) => {
739
+ if (!this.#watcher) {
740
+ this.#onError?.(error);
741
+ } else {
742
+ this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
743
+ }
744
+ }).finally(() => {
745
+ console.log("ere>>");
746
+ resolve();
747
+ });
748
+ });
729
749
  }
730
750
  /**
731
751
  * Add listener to get notified when dev server is
@@ -744,11 +764,10 @@ var DevServer = class {
744
764
  return this;
745
765
  }
746
766
  /**
747
- * Close watchers and running child processes
767
+ * Close watchers and the running child process
748
768
  */
749
769
  async close() {
750
770
  await this.#watcher?.close();
751
- this.#assetsServer?.stop();
752
771
  if (this.#httpServer) {
753
772
  this.#httpServer.removeAllListeners();
754
773
  this.#httpServer.kill("SIGKILL");
@@ -757,173 +776,175 @@ var DevServer = class {
757
776
  /**
758
777
  * Start the development server
759
778
  */
760
- async start() {
761
- await this.#hooks.registerDevServerHooks();
779
+ async start(ts) {
780
+ const tsConfig = parseConfig(this.cwd, ts);
781
+ if (!tsConfig) {
782
+ return;
783
+ }
784
+ this.#stickyPort = String(await getPort(this.cwd));
785
+ this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
786
+ this.#hooks = await loadHooks(this.options.hooks, [
787
+ "devServerStarting",
788
+ "devServerStarted",
789
+ "fileAdded",
790
+ "fileChanged",
791
+ "fileRemoved"
792
+ ]);
793
+ this.#registerServerRestartHooks();
794
+ if (this.options.hmr) {
795
+ this.#mode = "hmr";
796
+ this.options.nodeArgs = this.options.nodeArgs.concat("--import=hot-hook/register");
797
+ this.options.env = {
798
+ ...this.options.env,
799
+ HOT_HOOK_INCLUDE: this.#fileSystem.includes.join(","),
800
+ HOT_HOOK_IGNORE: this.#fileSystem.excludes.join(","),
801
+ HOT_HOOK_RESTART: (this.options.metaFiles ?? []).map(({ pattern }) => pattern).join(",")
802
+ };
803
+ }
762
804
  this.#clearScreen();
763
- this.#logger.info("starting HTTP server...");
764
- this.#startHTTPServer(String(await getPort(this.#cwd)), "nonblocking");
765
- this.#startAssetsServer();
805
+ this.ui.logger.info("starting HTTP server...");
806
+ await this.#startHTTPServer(this.#stickyPort);
766
807
  }
767
808
  /**
768
809
  * Start the development server in watch mode
769
810
  */
770
811
  async startAndWatch(ts, options) {
771
- await this.#hooks.registerDevServerHooks();
772
- const port = String(await getPort(this.#cwd));
773
- this.#isWatching = true;
774
- this.#clearScreen();
775
- this.#logger.info("starting HTTP server...");
776
- this.#startHTTPServer(port, "blocking");
777
- this.#startAssetsServer();
778
- const output = watch(this.#cwd, ts, options || {});
779
- if (!output) {
780
- this.#onClose?.(1);
812
+ const tsConfig = parseConfig(this.cwd, ts);
813
+ if (!tsConfig) {
781
814
  return;
782
815
  }
783
- this.#watcher = output.chokidar;
784
- output.watcher.on("watcher:ready", () => {
785
- this.#logger.info("watching file system for changes...");
816
+ this.#mode = "watch";
817
+ this.#stickyPort = String(await getPort(this.cwd));
818
+ this.#fileSystem = new FileSystem(this.cwd, tsConfig, this.options);
819
+ this.#hooks = await loadHooks(this.options.hooks, [
820
+ "devServerStarting",
821
+ "devServerStarted",
822
+ "fileAdded",
823
+ "fileChanged",
824
+ "fileRemoved"
825
+ ]);
826
+ this.#registerServerRestartHooks();
827
+ this.#clearScreen();
828
+ this.ui.logger.info("starting HTTP server...");
829
+ await this.#startHTTPServer(this.#stickyPort);
830
+ this.#watcher = watch({
831
+ usePolling: options?.poll ?? false,
832
+ cwd: fileURLToPath4(this.cwd),
833
+ ignoreInitial: true,
834
+ ignored: (file, stats) => {
835
+ if (!stats) {
836
+ return false;
837
+ }
838
+ if (stats.isFile()) {
839
+ return !this.#fileSystem.shouldWatchFile(file);
840
+ }
841
+ return !this.#fileSystem.shouldWatchDirectory(file);
842
+ }
843
+ });
844
+ this.#watcher.on("ready", () => {
845
+ this.ui.logger.info("watching file system for changes...");
786
846
  });
787
- output.chokidar.on("error", (error) => {
788
- this.#logger.warning("file system watcher failure");
789
- this.#logger.fatal(error);
847
+ this.#watcher.on("error", (error) => {
848
+ this.ui.logger.warning("file system watcher failure");
849
+ this.ui.logger.fatal(error);
790
850
  this.#onError?.(error);
791
- output.chokidar.close();
851
+ this.#watcher?.close();
792
852
  });
793
- output.watcher.on(
794
- "source:add",
795
- ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
796
- );
797
- output.watcher.on(
798
- "source:change",
799
- ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
800
- );
801
- output.watcher.on(
802
- "source:unlink",
803
- ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
804
- );
805
- output.watcher.on(
853
+ this.#watcher.on(
806
854
  "add",
807
- ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
855
+ (filePath) => this.#hooks.runner("fileAdded").run(string3.toUnixSlash(filePath), this)
808
856
  );
809
- output.watcher.on(
857
+ this.#watcher.on(
810
858
  "change",
811
- ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
859
+ (filePath) => this.#hooks.runner("fileChanged").run(string3.toUnixSlash(filePath), false, this)
812
860
  );
813
- output.watcher.on(
861
+ this.#watcher.on(
814
862
  "unlink",
815
- ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
863
+ (filePath) => this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(filePath), this)
816
864
  );
817
865
  }
818
866
  };
819
867
 
820
868
  // src/test_runner.ts
821
- import picomatch2 from "picomatch";
822
- import { cliui as cliui4 } from "@poppinss/cliui";
823
- var ui4 = cliui4();
869
+ import { cliui as cliui3 } from "@poppinss/cliui";
870
+ import { fileURLToPath as fileURLToPath5 } from "url";
871
+ import string4 from "@poppinss/utils/string";
824
872
  var TestRunner = class {
825
- #cwd;
826
- #logger = ui4.logger;
827
- #options;
828
- /**
829
- * The script file to run as a child process
830
- */
831
- #scriptFile = "bin/test.js";
832
- /**
833
- * Pico matcher function to check if the filepath is
834
- * part of the `metaFiles` glob patterns
835
- */
836
- #isMetaFile;
873
+ constructor(cwd, options) {
874
+ this.cwd = cwd;
875
+ this.options = options;
876
+ }
837
877
  /**
838
- * Pico matcher function to check if the filepath is
839
- * part of a test file.
878
+ * External listeners that are invoked when child process
879
+ * gets an error or closes
840
880
  */
841
- #isTestFile;
881
+ #onError;
882
+ #onClose;
842
883
  /**
843
- * Arguments to pass to the "bin/test.js" file.
884
+ * The stickyPort is set by the startAndWatch method and we will
885
+ * continue to use this port during re-runs
844
886
  */
845
- #scriptArgs;
887
+ #stickyPort;
846
888
  /**
847
- * Set of initial filters applied when running the test
848
- * command. In watch mode, we will append an additional
849
- * filter to run tests only for the file that has been
850
- * changed.
889
+ * Reference to chokidar watcher
851
890
  */
852
- #initialFiltersArgs;
891
+ #watcher;
853
892
  /**
854
- * In watch mode, after a file is changed, we wait for the current
855
- * set of tests to finish before triggering a re-run. Therefore,
856
- * we use this flag to know if we are already busy in running
857
- * tests and ignore file-changes.
893
+ * Reference to the test script child process
858
894
  */
859
- #isBusy = false;
895
+ #testsProcess;
860
896
  /**
861
- * External listeners that are invoked when child process
862
- * gets an error or closes
897
+ * Filesystem is used to decide which files to watch or entertain in watch
898
+ * mode
863
899
  */
864
- #onError;
865
- #onClose;
900
+ #fileSystem;
866
901
  /**
867
- * Reference to the test script child process
902
+ * Hooks to execute custom actions during the tests runner lifecycle
868
903
  */
869
- #testScript;
904
+ #hooks;
870
905
  /**
871
- * Reference to the watcher
906
+ * Re-runs the test child process and throttle concurrent calls to
907
+ * ensure we do not end up with a long loop of restarts
872
908
  */
873
- #watcher;
909
+ #reRunTests = throttle(async (filters) => {
910
+ if (this.#testsProcess) {
911
+ this.#testsProcess.removeAllListeners();
912
+ this.#testsProcess.kill("SIGKILL");
913
+ }
914
+ await this.#runTests(this.#stickyPort, filters);
915
+ }, "reRunTests");
874
916
  /**
875
- * Reference to the assets server
917
+ * CLI UI to log colorful messages
876
918
  */
877
- #assetsServer;
919
+ ui = cliui3();
878
920
  /**
879
- * Getting reference to colors library from logger
921
+ * The script file to run as a child process
880
922
  */
881
- get #colors() {
882
- return this.#logger.getColors();
883
- }
884
- constructor(cwd, options) {
885
- this.#cwd = cwd;
886
- this.#options = options;
887
- this.#isMetaFile = picomatch2((this.#options.metaFiles || []).map(({ pattern }) => pattern));
888
- this.#isTestFile = picomatch2(
889
- this.#options.suites.filter((suite) => {
890
- if (this.#options.filters.suites) {
891
- return this.#options.filters.suites.includes(suite.name);
892
- }
893
- return true;
894
- }).map((suite) => suite.files).flat(1)
895
- );
896
- this.#scriptArgs = this.#convertOptionsToArgs().concat(this.#options.scriptArgs);
897
- this.#initialFiltersArgs = this.#convertFiltersToArgs(this.#options.filters);
898
- }
923
+ scriptFile = "bin/test.ts";
899
924
  /**
900
925
  * Convert test runner options to the CLI args
901
926
  */
902
927
  #convertOptionsToArgs() {
903
928
  const args = [];
904
- if (this.#options.reporters) {
929
+ if (this.options.reporters) {
905
930
  args.push("--reporters");
906
- args.push(this.#options.reporters.join(","));
931
+ args.push(this.options.reporters.join(","));
907
932
  }
908
- if (this.#options.timeout !== void 0) {
933
+ if (this.options.timeout !== void 0) {
909
934
  args.push("--timeout");
910
- args.push(String(this.#options.timeout));
935
+ args.push(String(this.options.timeout));
911
936
  }
912
- if (this.#options.failed) {
937
+ if (this.options.failed) {
913
938
  args.push("--failed");
914
939
  }
915
- if (this.#options.retries !== void 0) {
940
+ if (this.options.retries !== void 0) {
916
941
  args.push("--retries");
917
- args.push(String(this.#options.retries));
942
+ args.push(String(this.options.retries));
918
943
  }
919
944
  return args;
920
945
  }
921
946
  /**
922
947
  * Converts all known filters to CLI args.
923
- *
924
- * The following code snippet may seem like repetitive code. But, it
925
- * is done intentionally to have visibility around how each filter
926
- * is converted to an arg.
927
948
  */
928
949
  #convertFiltersToArgs(filters) {
929
950
  const args = [];
@@ -952,93 +973,74 @@ var TestRunner = class {
952
973
  * Conditionally clear the terminal screen
953
974
  */
954
975
  #clearScreen() {
955
- if (this.#options.clearScreen) {
976
+ if (this.options.clearScreen) {
956
977
  process.stdout.write("\x1Bc");
957
978
  }
958
979
  }
959
980
  /**
960
981
  * Runs tests
961
982
  */
962
- #runTests(port, mode, filters) {
963
- this.#isBusy = true;
964
- const scriptArgs = filters ? this.#convertFiltersToArgs(filters).concat(this.#scriptArgs) : this.#initialFiltersArgs.concat(this.#scriptArgs);
965
- this.#testScript = runNode(this.#cwd, {
966
- script: this.#scriptFile,
967
- env: { PORT: port, ...this.#options.env },
968
- nodeArgs: this.#options.nodeArgs,
969
- scriptArgs
970
- });
971
- this.#testScript.then((result) => {
972
- if (mode === "nonblocking") {
973
- this.#onClose?.(result.exitCode);
974
- this.close();
975
- }
976
- }).catch((error) => {
977
- if (mode === "nonblocking") {
978
- this.#onError?.(error);
979
- this.close();
980
- }
981
- }).finally(() => {
982
- this.#isBusy = false;
983
+ async #runTests(port, filters) {
984
+ await this.#hooks.runner("testsStarting").run(this);
985
+ debug_default('running tests using "%s" file, options %O', this.scriptFile, this.options);
986
+ return new Promise(async (resolve) => {
987
+ const scriptArgs = this.#convertOptionsToArgs().concat(this.options.scriptArgs).concat(
988
+ this.#convertFiltersToArgs({
989
+ ...this.options.filters,
990
+ ...filters
991
+ })
992
+ );
993
+ this.#testsProcess = runNode(this.cwd, {
994
+ script: this.scriptFile,
995
+ reject: true,
996
+ env: { PORT: port, ...this.options.env },
997
+ nodeArgs: this.options.nodeArgs,
998
+ scriptArgs
999
+ });
1000
+ this.#testsProcess.then((result) => {
1001
+ this.#hooks.runner("testsFinished").run(this).catch((error) => {
1002
+ this.ui.logger.error('One of the "testsFinished" hooks failed');
1003
+ this.ui.logger.fatal(error);
1004
+ }).finally(() => {
1005
+ if (!this.#watcher) {
1006
+ this.#onClose?.(result.exitCode);
1007
+ this.close();
1008
+ }
1009
+ });
1010
+ }).catch((error) => {
1011
+ if (!this.#watcher) {
1012
+ this.#onError?.(error);
1013
+ this.close();
1014
+ } else {
1015
+ this.ui.logger.info("Underlying HTTP server died. Still watching for changes");
1016
+ }
1017
+ }).finally(() => resolve());
983
1018
  });
984
1019
  }
985
1020
  /**
986
- * Re-run tests with additional inline filters. Should be
987
- * executed in watch mode only.
988
- */
989
- #rerunTests(port, filters) {
990
- if (this.#testScript) {
991
- this.#testScript.removeAllListeners();
992
- this.#testScript.kill("SIGKILL");
993
- }
994
- this.#runTests(port, "blocking", filters);
995
- }
996
- /**
997
- * Starts the assets server
998
- */
999
- #startAssetsServer() {
1000
- this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
1001
- this.#assetsServer.setLogger(this.#logger);
1002
- this.#assetsServer.start();
1003
- }
1004
- /**
1005
- * Handles a non TypeScript file change
1006
- */
1007
- #handleFileChange(action, port, relativePath) {
1008
- if (this.#isBusy) {
1009
- return;
1010
- }
1011
- if (isDotEnvFile(relativePath) || this.#isMetaFile(relativePath)) {
1012
- this.#clearScreen();
1013
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
1014
- this.#rerunTests(port);
1015
- }
1016
- }
1017
- /**
1018
- * Handles TypeScript source file change
1021
+ * Handles file change event
1019
1022
  */
1020
- #handleSourceFileChange(action, port, relativePath) {
1021
- if (this.#isBusy) {
1023
+ #handleFileChange(filePath, action) {
1024
+ const file = this.#fileSystem.inspect(filePath);
1025
+ if (!file) {
1022
1026
  return;
1023
1027
  }
1024
1028
  this.#clearScreen();
1025
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
1026
- if (this.#isTestFile(relativePath)) {
1027
- this.#rerunTests(port, {
1028
- ...this.#options.filters,
1029
- files: [relativePath]
1030
- });
1031
- return;
1029
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
1030
+ if (file.fileType === "test") {
1031
+ this.#reRunTests({ files: [filePath] });
1032
+ } else {
1033
+ this.#reRunTests();
1032
1034
  }
1033
- this.#rerunTests(port);
1034
1035
  }
1035
1036
  /**
1036
- * Set a custom CLI UI logger
1037
+ * Registers inline hooks for the file changes and restarts the
1038
+ * HTTP server when a file gets changed.
1037
1039
  */
1038
- setLogger(logger) {
1039
- this.#logger = logger;
1040
- this.#assetsServer?.setLogger(logger);
1041
- return this;
1040
+ #registerServerRestartHooks() {
1041
+ this.#hooks.add("fileAdded", (filePath) => this.#handleFileChange(filePath, "add"));
1042
+ this.#hooks.add("fileChanged", (filePath) => this.#handleFileChange(filePath, "update"));
1043
+ this.#hooks.add("fileRemoved", (filePath) => this.#handleFileChange(filePath, "delete"));
1042
1044
  }
1043
1045
  /**
1044
1046
  * Add listener to get notified when dev server is
@@ -1061,69 +1063,90 @@ var TestRunner = class {
1061
1063
  */
1062
1064
  async close() {
1063
1065
  await this.#watcher?.close();
1064
- this.#assetsServer?.stop();
1065
- if (this.#testScript) {
1066
- this.#testScript.removeAllListeners();
1067
- this.#testScript.kill("SIGKILL");
1066
+ if (this.#testsProcess) {
1067
+ this.#testsProcess.removeAllListeners();
1068
+ this.#testsProcess.kill("SIGKILL");
1068
1069
  }
1069
1070
  }
1070
1071
  /**
1071
1072
  * Runs tests
1072
1073
  */
1073
1074
  async run() {
1074
- const port = String(await getPort(this.#cwd));
1075
+ this.#stickyPort = String(await getPort(this.cwd));
1076
+ this.#hooks = await loadHooks(this.options.hooks, [
1077
+ "testsStarting",
1078
+ "testsFinished",
1079
+ "fileAdded",
1080
+ "fileChanged",
1081
+ "fileRemoved"
1082
+ ]);
1075
1083
  this.#clearScreen();
1076
- this.#startAssetsServer();
1077
- this.#logger.info("booting application to run tests...");
1078
- this.#runTests(port, "nonblocking");
1084
+ this.ui.logger.info("booting application to run tests...");
1085
+ await this.#runTests(this.#stickyPort);
1079
1086
  }
1080
1087
  /**
1081
1088
  * Run tests in watch mode
1082
1089
  */
1083
1090
  async runAndWatch(ts, options) {
1084
- const port = String(await getPort(this.#cwd));
1085
- this.#clearScreen();
1086
- this.#startAssetsServer();
1087
- this.#logger.info("booting application to run tests...");
1088
- this.#runTests(port, "blocking");
1089
- const output = watch(this.#cwd, ts, options || {});
1090
- if (!output) {
1091
- this.#onClose?.(1);
1091
+ const tsConfig = parseConfig(this.cwd, ts);
1092
+ if (!tsConfig) {
1092
1093
  return;
1093
1094
  }
1094
- this.#watcher = output.chokidar;
1095
- output.watcher.on("watcher:ready", () => {
1096
- this.#logger.info("watching file system for changes...");
1095
+ this.#stickyPort = String(await getPort(this.cwd));
1096
+ this.#fileSystem = new FileSystem(this.cwd, tsConfig, {
1097
+ ...this.options,
1098
+ suites: this.options.suites?.filter((suite) => {
1099
+ if (this.options.filters.suites) {
1100
+ return this.options.filters.suites.includes(suite.name);
1101
+ }
1102
+ return true;
1103
+ })
1097
1104
  });
1098
- output.chokidar.on("error", (error) => {
1099
- this.#logger.warning("file system watcher failure");
1100
- this.#logger.fatal(error);
1105
+ this.#hooks = await loadHooks(this.options.hooks, [
1106
+ "testsStarting",
1107
+ "testsFinished",
1108
+ "fileAdded",
1109
+ "fileChanged",
1110
+ "fileRemoved"
1111
+ ]);
1112
+ this.#registerServerRestartHooks();
1113
+ this.#clearScreen();
1114
+ this.ui.logger.info("booting application to run tests...");
1115
+ await this.#runTests(this.#stickyPort);
1116
+ this.#watcher = watch({
1117
+ usePolling: options?.poll ?? false,
1118
+ cwd: fileURLToPath5(this.cwd),
1119
+ ignoreInitial: true,
1120
+ ignored: (file, stats) => {
1121
+ if (!stats) {
1122
+ return false;
1123
+ }
1124
+ if (stats.isFile()) {
1125
+ return !this.#fileSystem.shouldWatchFile(file);
1126
+ }
1127
+ return !this.#fileSystem.shouldWatchDirectory(file);
1128
+ }
1129
+ });
1130
+ this.#watcher.on("ready", () => {
1131
+ this.ui.logger.info("watching file system for changes...");
1132
+ });
1133
+ this.#watcher.on("error", (error) => {
1134
+ this.ui.logger.warning("file system watcher failure");
1135
+ this.ui.logger.fatal(error);
1101
1136
  this.#onError?.(error);
1102
- output.chokidar.close();
1137
+ this.#watcher?.close();
1103
1138
  });
1104
- output.watcher.on(
1105
- "source:add",
1106
- ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
1107
- );
1108
- output.watcher.on(
1109
- "source:change",
1110
- ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
1111
- );
1112
- output.watcher.on(
1113
- "source:unlink",
1114
- ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
1115
- );
1116
- output.watcher.on(
1139
+ this.#watcher.on(
1117
1140
  "add",
1118
- ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
1141
+ (filePath) => this.#hooks.runner("fileAdded").run(string4.toUnixSlash(filePath), this)
1119
1142
  );
1120
- output.watcher.on(
1143
+ this.#watcher.on(
1121
1144
  "change",
1122
- ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
1145
+ (filePath) => this.#hooks.runner("fileChanged").run(string4.toUnixSlash(filePath), false, this)
1123
1146
  );
1124
- output.watcher.on(
1147
+ this.#watcher.on(
1125
1148
  "unlink",
1126
- ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
1149
+ (filePath) => this.#hooks.runner("fileRemoved").run(string4.toUnixSlash(filePath), this)
1127
1150
  );
1128
1151
  }
1129
1152
  };
@@ -1132,4 +1155,3 @@ export {
1132
1155
  DevServer,
1133
1156
  TestRunner
1134
1157
  };
1135
- //# sourceMappingURL=index.js.map