@adonisjs/assembler 7.8.2 → 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,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);
255
+ async #detectPackageManager() {
256
+ const pkgManager = await detectPackageManager(this.#cwdPath);
257
+ if (pkgManager === "deno") {
258
+ return "npm";
320
259
  }
321
- if (!pkgManager) {
322
- pkgManager = "npm";
260
+ if (pkgManager === "pnpm@6") {
261
+ return "pnpm";
323
262
  }
324
- if (!Object.keys(SUPPORT_PACKAGE_MANAGERS).includes(pkgManager)) {
325
- return null;
326
- }
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,346 @@ 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 { cliui as cliui2 } from "@poppinss/cliui";
410
335
  import prettyHrtime from "pretty-hrtime";
411
- import { fileURLToPath as fileURLToPath3 } from "node:url";
412
- import { cliui as cliui3 } from "@poppinss/cliui";
336
+ import { fileURLToPath as fileURLToPath4 } from "url";
337
+ import string3 from "@poppinss/utils/string";
413
338
 
414
- // src/assets_dev_server.ts
415
- import { cliui as cliui2 } from "@poppinss/cliui";
416
- var ui2 = cliui2();
417
- 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
+ */
418
351
  #cwd;
419
- #logger = ui2.logger;
420
- #options;
421
- #devServer;
422
352
  /**
423
- * 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.
424
355
  */
425
- get #colors() {
426
- return this.#logger.getColors();
427
- }
428
- constructor(cwd, options) {
429
- this.#cwd = cwd;
430
- this.#options = options;
431
- }
356
+ #tsConfig;
432
357
  /**
433
- * Logs messages from vite dev server stdout and stderr
358
+ * Set of pre-scanned typeScript files provided by tsconfig
434
359
  */
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
- }
360
+ #scannedTypeScriptFiles = /* @__PURE__ */ new Set();
459
361
  /**
460
- * Logs messages from assets dev server stdout and stderr
362
+ * Picomatch matcher function to know if a file path is
363
+ * part of includes
461
364
  */
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
- }
365
+ #isIncluded;
471
366
  /**
472
- * Set a custom CLI UI logger
367
+ * Picomatch matcher function to know if a file path is
368
+ * part of excludes
473
369
  */
474
- setLogger(logger) {
475
- this.#logger = logger;
476
- 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;
477
397
  }
478
398
  /**
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.
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.
482
457
  */
483
- start() {
484
- if (!this.#options?.enabled) {
485
- return;
458
+ shouldWatchDirectory = memoize((absolutePath) => {
459
+ if (absolutePath === this.#cwd) {
460
+ debug_default("watching project root");
461
+ return true;
486
462
  }
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);
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
504
486
  }
505
- });
506
- this.#devServer.stderr?.on("data", (data) => {
507
- if (this.#options.driver === "vite") {
508
- this.#logViteDevServerMessage(data);
509
- } else {
510
- this.#logAssetsDevServerMessage(data);
487
+ );
488
+ this.#isMetaFileWithReloadsDisabled = picomatch(
489
+ metaFiles.filter((file) => !file.reloadServer).map((file) => file.pattern),
490
+ {
491
+ cwd: this.#cwd
511
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
512
502
  });
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);
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
520
513
  });
521
514
  }
522
515
  /**
523
- * 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.
524
521
  */
525
- stop() {
526
- if (this.#devServer) {
527
- this.#devServer.removeAllListeners();
528
- this.#devServer.kill("SIGKILL");
529
- 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;
530
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;
531
559
  }
532
560
  };
533
561
 
534
562
  // src/dev_server.ts
535
- var ui3 = cliui3();
536
563
  var DevServer = class {
537
- #cwd;
538
- #logger = ui3.logger;
539
- #options;
540
- /**
541
- * Flag to know if the dev server is running in watch
542
- * mode
543
- */
544
- #isWatching = false;
564
+ constructor(cwd, options) {
565
+ this.cwd = cwd;
566
+ this.options = options;
567
+ }
545
568
  /**
546
- * Script file to start the development server
569
+ * External listeners that are invoked when child process
570
+ * gets an error or closes
547
571
  */
548
- #scriptFile = "bin/server.js";
572
+ #onError;
573
+ #onClose;
549
574
  /**
550
- * Picomatch matcher function to know if a file path is a
551
- * 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
552
577
  */
553
- #isMetaFileWithReloadsEnabled;
578
+ #stickyPort;
554
579
  /**
555
- * Picomatch matcher function to know if a file path is a
556
- * meta file with reloadServer option disabled
580
+ * The mode is set by the start and the startAndWatch methods
557
581
  */
558
- #isMetaFileWithReloadsDisabled;
582
+ #mode = "static";
559
583
  /**
560
- * External listeners that are invoked when child process
561
- * gets an error or closes
584
+ * Reference to chokidar watcher
562
585
  */
563
- #onError;
564
- #onClose;
586
+ #watcher;
565
587
  /**
566
588
  * Reference to the child process
567
589
  */
568
590
  #httpServer;
569
591
  /**
570
- * Reference to the watcher
592
+ * Filesystem is used to decide which files to watch or entertain when
593
+ * using hot-hook
571
594
  */
572
- #watcher;
573
- /**
574
- * Reference to the assets server
575
- */
576
- #assetsServer;
595
+ #fileSystem;
577
596
  /**
578
597
  * Hooks to execute custom actions during the dev server lifecycle
579
598
  */
580
599
  #hooks;
581
600
  /**
582
- * 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
583
603
  */
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"]);
604
+ #restartHTTPServer = throttle(async () => {
605
+ if (this.#httpServer) {
606
+ this.#httpServer.removeAllListeners();
607
+ this.#httpServer.kill("SIGKILL");
593
608
  }
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
- );
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;
600
620
  }
621
+ /**
622
+ * Script file to start the development server
623
+ */
624
+ scriptFile = "bin/server.ts";
601
625
  /**
602
626
  * Inspect if child process message is from AdonisJS HTTP server
603
627
  */
@@ -605,7 +629,25 @@ var DevServer = class {
605
629
  return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
606
630
  }
607
631
  /**
608
- * 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
609
651
  */
610
652
  #isHotHookMessage(message) {
611
653
  return message !== null && typeof message === "object" && "type" in message && typeof message.type === "string" && message.type.startsWith("hot-hook:");
@@ -614,121 +656,96 @@ var DevServer = class {
614
656
  * Conditionally clear the terminal screen
615
657
  */
616
658
  #clearScreen() {
617
- if (this.#options.clearScreen) {
659
+ if (this.options.clearScreen) {
618
660
  process.stdout.write("\x1Bc");
619
661
  }
620
662
  }
621
663
  /**
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
664
+ * Handles file change event
678
665
  */
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
687
- */
688
- #restartHTTPServer(port) {
689
- if (this.#httpServer) {
690
- this.#httpServer.removeAllListeners();
691
- this.#httpServer.kill("SIGKILL");
692
- }
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);
666
+ #handleFileChange(filePath, action, hotReplaced) {
667
+ const file = this.#fileSystem.inspect(filePath);
668
+ if (!file) {
703
669
  return;
704
670
  }
705
- if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
706
- this.#clearScreen();
707
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
708
- this.#restartHTTPServer(port);
671
+ if (hotReplaced) {
672
+ this.ui.logger.log(`${this.ui.colors.green("invalidated")} ${filePath}`);
709
673
  return;
710
674
  }
711
- if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
675
+ if (file.reloadServer) {
712
676
  this.#clearScreen();
713
- this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
677
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
678
+ this.#restartHTTPServer();
679
+ return;
714
680
  }
681
+ this.ui.logger.log(`${this.ui.colors.green(action)} ${filePath}`);
715
682
  }
716
683
  /**
717
- * Handles TypeScript source file change
684
+ * Registers inline hooks for the file changes and restarts the
685
+ * HTTP server when a file gets changed.
718
686
  */
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);
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"));
724
694
  }
725
695
  /**
726
- * Set a custom CLI UI logger
696
+ * Starts the HTTP server
727
697
  */
728
- setLogger(logger) {
729
- this.#logger = logger;
730
- this.#assetsServer?.setLogger(logger);
731
- 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
+ });
732
749
  }
733
750
  /**
734
751
  * Add listener to get notified when dev server is
@@ -747,11 +764,10 @@ var DevServer = class {
747
764
  return this;
748
765
  }
749
766
  /**
750
- * Close watchers and running child processes
767
+ * Close watchers and the running child process
751
768
  */
752
769
  async close() {
753
770
  await this.#watcher?.close();
754
- this.#assetsServer?.stop();
755
771
  if (this.#httpServer) {
756
772
  this.#httpServer.removeAllListeners();
757
773
  this.#httpServer.kill("SIGKILL");
@@ -760,173 +776,175 @@ var DevServer = class {
760
776
  /**
761
777
  * Start the development server
762
778
  */
763
- async start() {
764
- 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
+ }
765
804
  this.#clearScreen();
766
- this.#logger.info("starting HTTP server...");
767
- this.#startHTTPServer(String(await getPort(this.#cwd)), "nonblocking");
768
- this.#startAssetsServer();
805
+ this.ui.logger.info("starting HTTP server...");
806
+ await this.#startHTTPServer(this.#stickyPort);
769
807
  }
770
808
  /**
771
809
  * Start the development server in watch mode
772
810
  */
773
811
  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);
812
+ const tsConfig = parseConfig(this.cwd, ts);
813
+ if (!tsConfig) {
784
814
  return;
785
815
  }
786
- this.#watcher = output.chokidar;
787
- output.watcher.on("watcher:ready", () => {
788
- 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...");
789
846
  });
790
- output.chokidar.on("error", (error) => {
791
- this.#logger.warning("file system watcher failure");
792
- 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);
793
850
  this.#onError?.(error);
794
- output.chokidar.close();
851
+ this.#watcher?.close();
795
852
  });
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(
853
+ this.#watcher.on(
809
854
  "add",
810
- ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
855
+ (filePath) => this.#hooks.runner("fileAdded").run(string3.toUnixSlash(filePath), this)
811
856
  );
812
- output.watcher.on(
857
+ this.#watcher.on(
813
858
  "change",
814
- ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
859
+ (filePath) => this.#hooks.runner("fileChanged").run(string3.toUnixSlash(filePath), false, this)
815
860
  );
816
- output.watcher.on(
861
+ this.#watcher.on(
817
862
  "unlink",
818
- ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
863
+ (filePath) => this.#hooks.runner("fileRemoved").run(string3.toUnixSlash(filePath), this)
819
864
  );
820
865
  }
821
866
  };
822
867
 
823
868
  // src/test_runner.ts
824
- import picomatch2 from "picomatch";
825
- import { cliui as cliui4 } from "@poppinss/cliui";
826
- 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";
827
872
  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";
873
+ constructor(cwd, options) {
874
+ this.cwd = cwd;
875
+ this.options = options;
876
+ }
835
877
  /**
836
- * Pico matcher function to check if the filepath is
837
- * part of the `metaFiles` glob patterns
878
+ * External listeners that are invoked when child process
879
+ * gets an error or closes
838
880
  */
839
- #isMetaFile;
881
+ #onError;
882
+ #onClose;
840
883
  /**
841
- * Pico matcher function to check if the filepath is
842
- * part of a test file.
884
+ * The stickyPort is set by the startAndWatch method and we will
885
+ * continue to use this port during re-runs
843
886
  */
844
- #isTestFile;
887
+ #stickyPort;
845
888
  /**
846
- * Arguments to pass to the "bin/test.js" file.
889
+ * Reference to chokidar watcher
847
890
  */
848
- #scriptArgs;
891
+ #watcher;
849
892
  /**
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.
893
+ * Reference to the test script child process
854
894
  */
855
- #initialFiltersArgs;
895
+ #testsProcess;
856
896
  /**
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.
897
+ * Filesystem is used to decide which files to watch or entertain in watch
898
+ * mode
861
899
  */
862
- #isBusy = false;
900
+ #fileSystem;
863
901
  /**
864
- * External listeners that are invoked when child process
865
- * gets an error or closes
902
+ * Hooks to execute custom actions during the tests runner lifecycle
866
903
  */
867
- #onError;
868
- #onClose;
904
+ #hooks;
869
905
  /**
870
- * Reference to the test script child process
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
871
908
  */
872
- #testScript;
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");
873
916
  /**
874
- * Reference to the watcher
917
+ * CLI UI to log colorful messages
875
918
  */
876
- #watcher;
919
+ ui = cliui3();
877
920
  /**
878
- * Reference to the assets server
921
+ * The script file to run as a child process
879
922
  */
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
- }
923
+ scriptFile = "bin/test.ts";
902
924
  /**
903
925
  * Convert test runner options to the CLI args
904
926
  */
905
927
  #convertOptionsToArgs() {
906
928
  const args = [];
907
- if (this.#options.reporters) {
929
+ if (this.options.reporters) {
908
930
  args.push("--reporters");
909
- args.push(this.#options.reporters.join(","));
931
+ args.push(this.options.reporters.join(","));
910
932
  }
911
- if (this.#options.timeout !== void 0) {
933
+ if (this.options.timeout !== void 0) {
912
934
  args.push("--timeout");
913
- args.push(String(this.#options.timeout));
935
+ args.push(String(this.options.timeout));
914
936
  }
915
- if (this.#options.failed) {
937
+ if (this.options.failed) {
916
938
  args.push("--failed");
917
939
  }
918
- if (this.#options.retries !== void 0) {
940
+ if (this.options.retries !== void 0) {
919
941
  args.push("--retries");
920
- args.push(String(this.#options.retries));
942
+ args.push(String(this.options.retries));
921
943
  }
922
944
  return args;
923
945
  }
924
946
  /**
925
947
  * 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
948
  */
931
949
  #convertFiltersToArgs(filters) {
932
950
  const args = [];
@@ -955,94 +973,74 @@ var TestRunner = class {
955
973
  * Conditionally clear the terminal screen
956
974
  */
957
975
  #clearScreen() {
958
- if (this.#options.clearScreen) {
976
+ if (this.options.clearScreen) {
959
977
  process.stdout.write("\x1Bc");
960
978
  }
961
979
  }
962
980
  /**
963
981
  * Runs tests
964
982
  */
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;
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());
987
1018
  });
988
1019
  }
989
1020
  /**
990
- * Re-run tests with additional inline filters. Should be
991
- * executed in watch mode only.
992
- */
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
1021
+ * Handles file change event
1023
1022
  */
1024
- #handleSourceFileChange(action, port, relativePath) {
1025
- if (this.#isBusy) {
1023
+ #handleFileChange(filePath, action) {
1024
+ const file = this.#fileSystem.inspect(filePath);
1025
+ if (!file) {
1026
1026
  return;
1027
1027
  }
1028
1028
  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;
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();
1036
1034
  }
1037
- this.#rerunTests(port);
1038
1035
  }
1039
1036
  /**
1040
- * 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.
1041
1039
  */
1042
- setLogger(logger) {
1043
- this.#logger = logger;
1044
- this.#assetsServer?.setLogger(logger);
1045
- 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"));
1046
1044
  }
1047
1045
  /**
1048
1046
  * Add listener to get notified when dev server is
@@ -1065,69 +1063,90 @@ var TestRunner = class {
1065
1063
  */
1066
1064
  async close() {
1067
1065
  await this.#watcher?.close();
1068
- this.#assetsServer?.stop();
1069
- if (this.#testScript) {
1070
- this.#testScript.removeAllListeners();
1071
- this.#testScript.kill("SIGKILL");
1066
+ if (this.#testsProcess) {
1067
+ this.#testsProcess.removeAllListeners();
1068
+ this.#testsProcess.kill("SIGKILL");
1072
1069
  }
1073
1070
  }
1074
1071
  /**
1075
1072
  * Runs tests
1076
1073
  */
1077
1074
  async run() {
1078
- 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
+ ]);
1079
1083
  this.#clearScreen();
1080
- this.#startAssetsServer();
1081
- this.#logger.info("booting application to run tests...");
1082
- this.#runTests(port, "nonblocking");
1084
+ this.ui.logger.info("booting application to run tests...");
1085
+ await this.#runTests(this.#stickyPort);
1083
1086
  }
1084
1087
  /**
1085
1088
  * Run tests in watch mode
1086
1089
  */
1087
1090
  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);
1091
+ const tsConfig = parseConfig(this.cwd, ts);
1092
+ if (!tsConfig) {
1096
1093
  return;
1097
1094
  }
1098
- this.#watcher = output.chokidar;
1099
- output.watcher.on("watcher:ready", () => {
1100
- 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
+ })
1101
1104
  });
1102
- output.chokidar.on("error", (error) => {
1103
- this.#logger.warning("file system watcher failure");
1104
- 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);
1105
1136
  this.#onError?.(error);
1106
- output.chokidar.close();
1137
+ this.#watcher?.close();
1107
1138
  });
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(
1139
+ this.#watcher.on(
1121
1140
  "add",
1122
- ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
1141
+ (filePath) => this.#hooks.runner("fileAdded").run(string4.toUnixSlash(filePath), this)
1123
1142
  );
1124
- output.watcher.on(
1143
+ this.#watcher.on(
1125
1144
  "change",
1126
- ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
1145
+ (filePath) => this.#hooks.runner("fileChanged").run(string4.toUnixSlash(filePath), false, this)
1127
1146
  );
1128
- output.watcher.on(
1147
+ this.#watcher.on(
1129
1148
  "unlink",
1130
- ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
1149
+ (filePath) => this.#hooks.runner("fileRemoved").run(string4.toUnixSlash(filePath), this)
1131
1150
  );
1132
1151
  }
1133
1152
  };
@@ -1136,4 +1155,3 @@ export {
1136
1155
  DevServer,
1137
1156
  TestRunner
1138
1157
  };
1139
- //# sourceMappingURL=index.js.map