@adonisjs/assembler 6.1.3-23 → 6.1.3-24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,11 +1,913 @@
1
- /*
2
- * @adonisjs/assembler
3
- *
4
- * (c) AdonisJS
5
- *
6
- * For the full copyright and license information, please view the LICENSE
7
- * file that was distributed with this source code.
8
- */
9
- export { Bundler } from './src/bundler.js';
10
- export { DevServer } from './src/dev_server.js';
11
- export { TestRunner } from './src/test_runner.js';
1
+ // src/bundler.ts
2
+ import slash from "slash";
3
+ import fs from "node:fs/promises";
4
+ import { relative as relative2 } from "node:path";
5
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
6
+ import { cliui } from "@poppinss/cliui";
7
+
8
+ // src/helpers.ts
9
+ import cpy from "cpy";
10
+ import { isNotJunk } from "junk";
11
+ import fastGlob from "fast-glob";
12
+ import getRandomPort from "get-port";
13
+ import { fileURLToPath } from "node:url";
14
+ import { execaNode, execa } from "execa";
15
+ import { isAbsolute, relative } from "node:path";
16
+ import { EnvLoader, EnvParser } from "@adonisjs/env";
17
+ import { ConfigParser, Watcher } from "@poppinss/chokidar-ts";
18
+
19
+ // src/debug.ts
20
+ import { debuglog } from "node:util";
21
+ var debug_default = debuglog("adonisjs:assembler");
22
+
23
+ // src/helpers.ts
24
+ var DEFAULT_NODE_ARGS = [
25
+ // Use ts-node/esm loader. The project must install it
26
+ "--loader=ts-node/esm",
27
+ // Disable annonying warnings
28
+ "--no-warnings",
29
+ // Enable expiremental meta resolve for cases where someone uses magic import string
30
+ "--experimental-import-meta-resolve",
31
+ // Enable source maps, since TSNode source maps are broken
32
+ "--enable-source-maps"
33
+ ];
34
+ function parseConfig(cwd, ts) {
35
+ const { config, error } = new ConfigParser(cwd, "tsconfig.json", ts).parse();
36
+ if (error) {
37
+ const compilerHost = ts.createCompilerHost({});
38
+ console.log(ts.formatDiagnosticsWithColorAndContext([error], compilerHost));
39
+ return;
40
+ }
41
+ if (config.errors.length) {
42
+ const compilerHost = ts.createCompilerHost({});
43
+ console.log(ts.formatDiagnosticsWithColorAndContext(config.errors, compilerHost));
44
+ return;
45
+ }
46
+ return config;
47
+ }
48
+ function runNode(cwd, options) {
49
+ const childProcess = execaNode(options.script, options.scriptArgs, {
50
+ nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs),
51
+ preferLocal: true,
52
+ windowsHide: false,
53
+ localDir: cwd,
54
+ cwd,
55
+ buffer: false,
56
+ stdio: options.stdio || "inherit",
57
+ env: {
58
+ ...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
59
+ ...options.env
60
+ }
61
+ });
62
+ return childProcess;
63
+ }
64
+ function run(cwd, options) {
65
+ const childProcess = execa(options.script, options.scriptArgs, {
66
+ preferLocal: true,
67
+ windowsHide: false,
68
+ localDir: cwd,
69
+ cwd,
70
+ buffer: false,
71
+ stdio: options.stdio || "inherit",
72
+ env: {
73
+ ...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
74
+ ...options.env
75
+ }
76
+ });
77
+ return childProcess;
78
+ }
79
+ function watch(cwd, ts, options) {
80
+ const config = parseConfig(cwd, ts);
81
+ if (!config) {
82
+ return;
83
+ }
84
+ const watcher = new Watcher(typeof cwd === "string" ? cwd : fileURLToPath(cwd), config);
85
+ const chokidar = watcher.watch(["."], { usePolling: options.poll });
86
+ return { watcher, chokidar };
87
+ }
88
+ function isDotEnvFile(filePath) {
89
+ if (filePath === ".env") {
90
+ return true;
91
+ }
92
+ return filePath.includes(".env.");
93
+ }
94
+ function isRcFile(filePath) {
95
+ return filePath === ".adonisrc.json";
96
+ }
97
+ async function getPort(cwd) {
98
+ if (process.env.PORT) {
99
+ return getRandomPort({ port: Number(process.env.PORT) });
100
+ }
101
+ const files = await new EnvLoader(cwd).load();
102
+ for (let file of files) {
103
+ const envVariables = new EnvParser(file.contents).parse();
104
+ if (envVariables.PORT) {
105
+ return getRandomPort({ port: Number(envVariables.PORT) });
106
+ }
107
+ }
108
+ return getRandomPort({ port: 3333 });
109
+ }
110
+ async function copyFiles(files, cwd, outDir) {
111
+ const { paths, patterns } = files.reduce(
112
+ (result, file) => {
113
+ if (fastGlob.isDynamicPattern(file)) {
114
+ result.patterns.push(file);
115
+ } else {
116
+ result.paths.push(file);
117
+ }
118
+ return result;
119
+ },
120
+ { patterns: [], paths: [] }
121
+ );
122
+ debug_default("copyFiles inputs: %O, paths: %O, patterns: %O", files, paths, patterns);
123
+ const filePaths = paths.concat(await fastGlob(patterns, { cwd }));
124
+ const destination = isAbsolute(outDir) ? relative(cwd, outDir) : outDir;
125
+ debug_default('copying files %O to destination "%s"', filePaths, destination);
126
+ return cpy(filePaths.filter(isNotJunk), destination, {
127
+ cwd,
128
+ flat: false
129
+ });
130
+ }
131
+
132
+ // src/bundler.ts
133
+ var ui = cliui();
134
+ var Bundler = class {
135
+ #cwd;
136
+ #cwdPath;
137
+ #ts;
138
+ #logger = ui.logger;
139
+ #options;
140
+ /**
141
+ * Getting reference to colors library from logger
142
+ */
143
+ get #colors() {
144
+ return this.#logger.getColors();
145
+ }
146
+ constructor(cwd, ts, options) {
147
+ this.#cwd = cwd;
148
+ this.#cwdPath = fileURLToPath2(this.#cwd);
149
+ this.#ts = ts;
150
+ this.#options = options;
151
+ }
152
+ #getRelativeName(filePath) {
153
+ return slash(relative2(this.#cwdPath, filePath));
154
+ }
155
+ /**
156
+ * Cleans up the build directory
157
+ */
158
+ async #cleanupBuildDirectory(outDir) {
159
+ await fs.rm(outDir, { recursive: true, force: true, maxRetries: 5 });
160
+ }
161
+ /**
162
+ * Runs assets bundler command to build assets.
163
+ */
164
+ async #buildAssets() {
165
+ const assetsBundler = this.#options.assets;
166
+ if (!assetsBundler?.serve) {
167
+ return true;
168
+ }
169
+ try {
170
+ this.#logger.info("compiling frontend assets", { suffix: assetsBundler.cmd });
171
+ await run(this.#cwd, {
172
+ stdio: "inherit",
173
+ script: assetsBundler.cmd,
174
+ scriptArgs: assetsBundler.args
175
+ });
176
+ return true;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+ /**
182
+ * Runs tsc command to build the source.
183
+ */
184
+ async #runTsc(outDir) {
185
+ try {
186
+ await run(this.#cwd, {
187
+ stdio: "inherit",
188
+ script: "tsc",
189
+ scriptArgs: ["--outDir", outDir]
190
+ });
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+ /**
197
+ * Copy files to destination directory
198
+ */
199
+ async #copyFiles(files, outDir) {
200
+ try {
201
+ await copyFiles(files, this.#cwdPath, outDir);
202
+ } catch (error) {
203
+ if (!error.message.includes("the file doesn't exist")) {
204
+ throw error;
205
+ }
206
+ }
207
+ }
208
+ /**
209
+ * Copy meta files to the output directory
210
+ */
211
+ async #copyMetaFiles(outDir, additionalFilesToCopy) {
212
+ const metaFiles = (this.#options.metaFiles || []).map((file) => file.pattern).concat(additionalFilesToCopy);
213
+ await this.#copyFiles(metaFiles, outDir);
214
+ }
215
+ /**
216
+ * Returns the lock file name for a given packages client
217
+ */
218
+ #getClientLockFile(client) {
219
+ switch (client) {
220
+ case "npm":
221
+ return "package-lock.json";
222
+ case "yarn":
223
+ return "yarn.lock";
224
+ case "pnpm":
225
+ return "pnpm-lock.yaml";
226
+ }
227
+ }
228
+ /**
229
+ * Returns the installation command for a given packages client
230
+ */
231
+ #getClientInstallCommand(client) {
232
+ switch (client) {
233
+ case "npm":
234
+ return 'npm ci --omit="dev"';
235
+ case "yarn":
236
+ return "yarn install --production";
237
+ case "pnpm":
238
+ return "pnpm i --prod";
239
+ }
240
+ }
241
+ /**
242
+ * Set a custom CLI UI logger
243
+ */
244
+ setLogger(logger) {
245
+ this.#logger = logger;
246
+ return this;
247
+ }
248
+ /**
249
+ * Bundles the application to be run in production
250
+ */
251
+ async bundle(stopOnError = true, client = "npm") {
252
+ const config = parseConfig(this.#cwd, this.#ts);
253
+ if (!config) {
254
+ return false;
255
+ }
256
+ const outDir = config.options.outDir || fileURLToPath2(new URL("build/", this.#cwd));
257
+ this.#logger.info("cleaning up output directory", { suffix: this.#getRelativeName(outDir) });
258
+ await this.#cleanupBuildDirectory(outDir);
259
+ if (!await this.#buildAssets()) {
260
+ return false;
261
+ }
262
+ this.#logger.info("compiling typescript source", { suffix: "tsc" });
263
+ const buildCompleted = await this.#runTsc(outDir);
264
+ await this.#copyFiles(["ace.js"], outDir);
265
+ if (!buildCompleted && stopOnError) {
266
+ await this.#cleanupBuildDirectory(outDir);
267
+ const instructions = ui.sticker().fullScreen().drawBorder((borderChar, colors) => colors.red(borderChar));
268
+ instructions.add(
269
+ this.#colors.red("Cannot complete the build process as there are TypeScript errors.")
270
+ );
271
+ instructions.add(
272
+ this.#colors.red(
273
+ 'Use "--ignore-ts-errors" flag to ignore TypeScript errors and continue the build.'
274
+ )
275
+ );
276
+ this.#logger.logError(instructions.prepare());
277
+ return false;
278
+ }
279
+ const pkgFiles = ["package.json", this.#getClientLockFile(client)];
280
+ this.#logger.info("copying meta files to the output directory");
281
+ await this.#copyMetaFiles(outDir, pkgFiles);
282
+ this.#logger.success("build completed");
283
+ this.#logger.log("");
284
+ 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(this.#colors.cyan(this.#getClientInstallCommand(client))).add(this.#colors.cyan("node bin/server.js")).render();
285
+ return true;
286
+ }
287
+ };
288
+
289
+ // src/dev_server.ts
290
+ import picomatch from "picomatch";
291
+ import { cliui as cliui3 } from "@poppinss/cliui";
292
+
293
+ // src/assets_dev_server.ts
294
+ import { cliui as cliui2 } from "@poppinss/cliui";
295
+ var ui2 = cliui2();
296
+ var AssetsDevServer = class {
297
+ #cwd;
298
+ #logger = ui2.logger;
299
+ #options;
300
+ #devServer;
301
+ /**
302
+ * Getting reference to colors library from logger
303
+ */
304
+ get #colors() {
305
+ return this.#logger.getColors();
306
+ }
307
+ constructor(cwd, options) {
308
+ this.#cwd = cwd;
309
+ this.#options = options;
310
+ }
311
+ /**
312
+ * Logs messages from vite dev server stdout and stderr
313
+ */
314
+ #logViteDevServerMessage(data) {
315
+ const dataString = data.toString();
316
+ const lines = dataString.split("\n");
317
+ if (dataString.includes("ready in")) {
318
+ console.log("");
319
+ console.log(dataString.trim());
320
+ return;
321
+ }
322
+ if (dataString.includes("Local") && dataString.includes("Network")) {
323
+ const sticker = ui2.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer());
324
+ lines.forEach((line) => {
325
+ if (line.trim()) {
326
+ sticker.add(line);
327
+ }
328
+ });
329
+ sticker.render();
330
+ return;
331
+ }
332
+ lines.forEach((line) => {
333
+ if (line.trim()) {
334
+ console.log(line);
335
+ }
336
+ });
337
+ }
338
+ /**
339
+ * Logs messages from assets dev server stdout and stderr
340
+ */
341
+ #logAssetsDevServerMessage(data) {
342
+ const dataString = data.toString();
343
+ const lines = dataString.split("\n");
344
+ lines.forEach((line) => {
345
+ if (line.trim()) {
346
+ console.log(line);
347
+ }
348
+ });
349
+ }
350
+ /**
351
+ * Set a custom CLI UI logger
352
+ */
353
+ setLogger(logger) {
354
+ this.#logger = logger;
355
+ return this;
356
+ }
357
+ /**
358
+ * Starts the assets bundler server. The assets bundler server process is
359
+ * considered as the secondary process and therefore we do not perform
360
+ * any cleanup if it dies.
361
+ */
362
+ start() {
363
+ if (!this.#options?.serve) {
364
+ return;
365
+ }
366
+ this.#logger.info(`starting "${this.#options.driver}" dev server...`);
367
+ this.#devServer = run(this.#cwd, {
368
+ script: this.#options.cmd,
369
+ /**
370
+ * We do not inherit the stdio for vite and encore, because in
371
+ * inherit mode they own the stdin and interrupts the
372
+ * `Ctrl + C` command.
373
+ */
374
+ stdio: "pipe",
375
+ scriptArgs: this.#options.args
376
+ });
377
+ this.#devServer.stdout?.on("data", (data) => {
378
+ if (this.#options.driver === "vite") {
379
+ this.#logViteDevServerMessage(data);
380
+ } else {
381
+ this.#logAssetsDevServerMessage(data);
382
+ }
383
+ });
384
+ this.#devServer.stderr?.on("data", (data) => {
385
+ if (this.#options.driver === "vite") {
386
+ this.#logViteDevServerMessage(data);
387
+ } else {
388
+ this.#logAssetsDevServerMessage(data);
389
+ }
390
+ });
391
+ this.#devServer.then((result) => {
392
+ this.#logger.warning(
393
+ `"${this.#options.driver}" dev server closed with status code "${result.exitCode}"`
394
+ );
395
+ }).catch((error) => {
396
+ this.#logger.warning(`unable to connect to "${this.#options.driver}" dev server`);
397
+ this.#logger.fatal(error);
398
+ });
399
+ }
400
+ /**
401
+ * Stop the dev server
402
+ */
403
+ stop() {
404
+ if (this.#devServer) {
405
+ this.#devServer.removeAllListeners();
406
+ this.#devServer.kill("SIGKILL");
407
+ this.#devServer = void 0;
408
+ }
409
+ }
410
+ };
411
+
412
+ // src/dev_server.ts
413
+ var ui3 = cliui3();
414
+ var DevServer = class {
415
+ #cwd;
416
+ #logger = ui3.logger;
417
+ #options;
418
+ #isWatching = false;
419
+ #scriptFile = "bin/server.js";
420
+ #isMetaFileWithReloadsEnabled;
421
+ #isMetaFileWithReloadsDisabled;
422
+ #onError;
423
+ #onClose;
424
+ #httpServer;
425
+ #watcher;
426
+ #assetsServer;
427
+ /**
428
+ * Getting reference to colors library from logger
429
+ */
430
+ get #colors() {
431
+ return this.#logger.getColors();
432
+ }
433
+ constructor(cwd, options) {
434
+ this.#cwd = cwd;
435
+ this.#options = options;
436
+ this.#isMetaFileWithReloadsEnabled = picomatch(
437
+ (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer === true).map(({ pattern }) => pattern)
438
+ );
439
+ this.#isMetaFileWithReloadsDisabled = picomatch(
440
+ (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer !== true).map(({ pattern }) => pattern)
441
+ );
442
+ }
443
+ /**
444
+ * Inspect if child process message is from AdonisJS HTTP server
445
+ */
446
+ #isAdonisJSReadyMessage(message) {
447
+ return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
448
+ }
449
+ /**
450
+ * Conditionally clear the terminal screen
451
+ */
452
+ #clearScreen() {
453
+ if (this.#options.clearScreen) {
454
+ process.stdout.write("\x1Bc");
455
+ }
456
+ }
457
+ /**
458
+ * Starts the HTTP server
459
+ */
460
+ #startHTTPServer(port, mode) {
461
+ this.#httpServer = runNode(this.#cwd, {
462
+ script: this.#scriptFile,
463
+ env: { PORT: port, ...this.#options.env },
464
+ nodeArgs: this.#options.nodeArgs,
465
+ scriptArgs: this.#options.scriptArgs
466
+ });
467
+ this.#httpServer.on("message", (message) => {
468
+ if (this.#isAdonisJSReadyMessage(message)) {
469
+ ui3.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer()).add(`Server address: ${this.#colors.cyan(`http://${message.host}:${message.port}`)}`).add(
470
+ `File system watcher: ${this.#colors.cyan(
471
+ `${this.#isWatching ? "enabled" : "disabled"}`
472
+ )}`
473
+ ).render();
474
+ }
475
+ });
476
+ this.#httpServer.then((result) => {
477
+ if (mode === "nonblocking") {
478
+ this.#onClose?.(result.exitCode);
479
+ this.#watcher?.close();
480
+ this.#assetsServer?.stop();
481
+ }
482
+ }).catch((error) => {
483
+ if (mode === "nonblocking") {
484
+ this.#onError?.(error);
485
+ this.#watcher?.close();
486
+ this.#assetsServer?.stop();
487
+ }
488
+ });
489
+ }
490
+ /**
491
+ * Starts the assets server
492
+ */
493
+ #startAssetsServer() {
494
+ this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
495
+ this.#assetsServer.setLogger(this.#logger);
496
+ this.#assetsServer.start();
497
+ }
498
+ /**
499
+ * Restarts the HTTP server
500
+ */
501
+ #restartHTTPServer(port) {
502
+ if (this.#httpServer) {
503
+ this.#httpServer.removeAllListeners();
504
+ this.#httpServer.kill("SIGKILL");
505
+ }
506
+ this.#startHTTPServer(port, "blocking");
507
+ }
508
+ /**
509
+ * Handles a non TypeScript file change
510
+ */
511
+ #handleFileChange(action, port, relativePath) {
512
+ if (isDotEnvFile(relativePath) || isRcFile(relativePath)) {
513
+ this.#clearScreen();
514
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
515
+ this.#restartHTTPServer(port);
516
+ return;
517
+ }
518
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
519
+ this.#clearScreen();
520
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
521
+ this.#restartHTTPServer(port);
522
+ return;
523
+ }
524
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
525
+ this.#clearScreen();
526
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
527
+ }
528
+ }
529
+ /**
530
+ * Handles TypeScript source file change
531
+ */
532
+ #handleSourceFileChange(action, port, relativePath) {
533
+ this.#clearScreen();
534
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
535
+ this.#restartHTTPServer(port);
536
+ }
537
+ /**
538
+ * Set a custom CLI UI logger
539
+ */
540
+ setLogger(logger) {
541
+ this.#logger = logger;
542
+ this.#assetsServer?.setLogger(logger);
543
+ return this;
544
+ }
545
+ /**
546
+ * Add listener to get notified when dev server is
547
+ * closed
548
+ */
549
+ onClose(callback) {
550
+ this.#onClose = callback;
551
+ return this;
552
+ }
553
+ /**
554
+ * Add listener to get notified when dev server exists
555
+ * with an error
556
+ */
557
+ onError(callback) {
558
+ this.#onError = callback;
559
+ return this;
560
+ }
561
+ /**
562
+ * Close watchers and running child processes
563
+ */
564
+ async close() {
565
+ await this.#watcher?.close();
566
+ this.#assetsServer?.stop();
567
+ if (this.#httpServer) {
568
+ this.#httpServer.removeAllListeners();
569
+ this.#httpServer.kill("SIGKILL");
570
+ }
571
+ }
572
+ /**
573
+ * Start the development server
574
+ */
575
+ async start() {
576
+ this.#clearScreen();
577
+ this.#logger.info("starting HTTP server...");
578
+ this.#startHTTPServer(String(await getPort(this.#cwd)), "nonblocking");
579
+ this.#startAssetsServer();
580
+ }
581
+ /**
582
+ * Start the development server in watch mode
583
+ */
584
+ async startAndWatch(ts, options) {
585
+ const port = String(await getPort(this.#cwd));
586
+ this.#isWatching = true;
587
+ this.#clearScreen();
588
+ this.#logger.info("starting HTTP server...");
589
+ this.#startHTTPServer(port, "blocking");
590
+ this.#startAssetsServer();
591
+ const output = watch(this.#cwd, ts, options || {});
592
+ if (!output) {
593
+ this.#onClose?.(1);
594
+ return;
595
+ }
596
+ this.#watcher = output.chokidar;
597
+ output.watcher.on("watcher:ready", () => {
598
+ this.#logger.info("watching file system for changes...");
599
+ });
600
+ output.chokidar.on("error", (error) => {
601
+ this.#logger.warning("file system watcher failure");
602
+ this.#logger.fatal(error);
603
+ this.#onError?.(error);
604
+ output.chokidar.close();
605
+ });
606
+ output.watcher.on(
607
+ "source:add",
608
+ ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
609
+ );
610
+ output.watcher.on(
611
+ "source:change",
612
+ ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
613
+ );
614
+ output.watcher.on(
615
+ "source:unlink",
616
+ ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
617
+ );
618
+ output.watcher.on(
619
+ "add",
620
+ ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
621
+ );
622
+ output.watcher.on(
623
+ "change",
624
+ ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
625
+ );
626
+ output.watcher.on(
627
+ "unlink",
628
+ ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
629
+ );
630
+ }
631
+ };
632
+
633
+ // src/test_runner.ts
634
+ import picomatch2 from "picomatch";
635
+ import { cliui as cliui4 } from "@poppinss/cliui";
636
+ var ui4 = cliui4();
637
+ var TestRunner = class {
638
+ #cwd;
639
+ #logger = ui4.logger;
640
+ #options;
641
+ #scriptFile = "bin/test.js";
642
+ #isMetaFile;
643
+ #isTestFile;
644
+ #scriptArgs;
645
+ #initialFiltersArgs;
646
+ /**
647
+ * In watch mode, after a file is changed, we wait for the current
648
+ * set of tests to finish before triggering a re-run. Therefore,
649
+ * we use this flag to know if we are already busy in running
650
+ * tests and ignore file-changes.
651
+ */
652
+ #isBusy = false;
653
+ #onError;
654
+ #onClose;
655
+ #testScript;
656
+ #watcher;
657
+ #assetsServer;
658
+ /**
659
+ * Getting reference to colors library from logger
660
+ */
661
+ get #colors() {
662
+ return this.#logger.getColors();
663
+ }
664
+ constructor(cwd, options) {
665
+ this.#cwd = cwd;
666
+ this.#options = options;
667
+ this.#isMetaFile = picomatch2((this.#options.metaFiles || []).map(({ pattern }) => pattern));
668
+ this.#isTestFile = picomatch2(
669
+ this.#options.suites.filter((suite) => {
670
+ if (this.#options.filters.suites) {
671
+ return this.#options.filters.suites.includes(suite.name);
672
+ }
673
+ return true;
674
+ }).map((suite) => suite.files).flat(1)
675
+ );
676
+ this.#scriptArgs = this.#convertOptionsToArgs().concat(this.#options.scriptArgs);
677
+ this.#initialFiltersArgs = this.#convertFiltersToArgs(this.#options.filters);
678
+ }
679
+ /**
680
+ * Converts options to CLI args
681
+ */
682
+ #convertOptionsToArgs() {
683
+ const args = [];
684
+ if (this.#options.reporters) {
685
+ args.push("--reporters");
686
+ args.push(this.#options.reporters.join(","));
687
+ }
688
+ if (this.#options.timeout !== void 0) {
689
+ args.push("--timeout");
690
+ args.push(String(this.#options.timeout));
691
+ }
692
+ if (this.#options.failed) {
693
+ args.push("--failed");
694
+ }
695
+ if (this.#options.retries !== void 0) {
696
+ args.push("--retries");
697
+ args.push(String(this.#options.retries));
698
+ }
699
+ return args;
700
+ }
701
+ /**
702
+ * Converts all known filters to CLI args.
703
+ *
704
+ * The following code snippet may seem like repetitive code. But, it
705
+ * is done intentionally to have visibility around how each filter
706
+ * is converted to an arg.
707
+ */
708
+ #convertFiltersToArgs(filters) {
709
+ const args = [];
710
+ if (filters.suites) {
711
+ args.push(...filters.suites);
712
+ }
713
+ if (filters.files) {
714
+ args.push("--files");
715
+ args.push(filters.files.join(","));
716
+ }
717
+ if (filters.groups) {
718
+ args.push("--groups");
719
+ args.push(filters.groups.join(","));
720
+ }
721
+ if (filters.tags) {
722
+ args.push("--tags");
723
+ args.push(filters.tags.join(","));
724
+ }
725
+ if (filters.tests) {
726
+ args.push("--tests");
727
+ args.push(filters.tests.join(","));
728
+ }
729
+ return args;
730
+ }
731
+ /**
732
+ * Conditionally clear the terminal screen
733
+ */
734
+ #clearScreen() {
735
+ if (this.#options.clearScreen) {
736
+ process.stdout.write("\x1Bc");
737
+ }
738
+ }
739
+ /**
740
+ * Runs tests
741
+ */
742
+ #runTests(port, mode, filters) {
743
+ this.#isBusy = true;
744
+ const scriptArgs = filters ? this.#convertFiltersToArgs(filters).concat(this.#scriptArgs) : this.#initialFiltersArgs.concat(this.#scriptArgs);
745
+ this.#testScript = runNode(this.#cwd, {
746
+ script: this.#scriptFile,
747
+ env: { PORT: port, ...this.#options.env },
748
+ nodeArgs: this.#options.nodeArgs,
749
+ scriptArgs
750
+ });
751
+ this.#testScript.then((result) => {
752
+ if (mode === "nonblocking") {
753
+ this.#onClose?.(result.exitCode);
754
+ this.close();
755
+ }
756
+ }).catch((error) => {
757
+ if (mode === "nonblocking") {
758
+ this.#onError?.(error);
759
+ this.close();
760
+ }
761
+ }).finally(() => {
762
+ this.#isBusy = false;
763
+ });
764
+ }
765
+ /**
766
+ * Restarts the HTTP server
767
+ */
768
+ #rerunTests(port, filters) {
769
+ if (this.#testScript) {
770
+ this.#testScript.removeAllListeners();
771
+ this.#testScript.kill("SIGKILL");
772
+ }
773
+ this.#runTests(port, "blocking", filters);
774
+ }
775
+ /**
776
+ * Starts the assets server
777
+ */
778
+ #startAssetsServer() {
779
+ this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
780
+ this.#assetsServer.setLogger(this.#logger);
781
+ this.#assetsServer.start();
782
+ }
783
+ /**
784
+ * Handles a non TypeScript file change
785
+ */
786
+ #handleFileChange(action, port, relativePath) {
787
+ if (this.#isBusy) {
788
+ return;
789
+ }
790
+ if (isDotEnvFile(relativePath) || this.#isMetaFile(relativePath)) {
791
+ this.#clearScreen();
792
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
793
+ this.#rerunTests(port);
794
+ }
795
+ }
796
+ /**
797
+ * Handles TypeScript source file change
798
+ */
799
+ #handleSourceFileChange(action, port, relativePath) {
800
+ if (this.#isBusy) {
801
+ return;
802
+ }
803
+ this.#clearScreen();
804
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
805
+ if (this.#isTestFile(relativePath)) {
806
+ this.#rerunTests(port, {
807
+ ...this.#options.filters,
808
+ files: [relativePath]
809
+ });
810
+ return;
811
+ }
812
+ this.#rerunTests(port);
813
+ }
814
+ /**
815
+ * Set a custom CLI UI logger
816
+ */
817
+ setLogger(logger) {
818
+ this.#logger = logger;
819
+ this.#assetsServer?.setLogger(logger);
820
+ return this;
821
+ }
822
+ /**
823
+ * Add listener to get notified when dev server is
824
+ * closed
825
+ */
826
+ onClose(callback) {
827
+ this.#onClose = callback;
828
+ return this;
829
+ }
830
+ /**
831
+ * Add listener to get notified when dev server exists
832
+ * with an error
833
+ */
834
+ onError(callback) {
835
+ this.#onError = callback;
836
+ return this;
837
+ }
838
+ /**
839
+ * Close watchers and running child processes
840
+ */
841
+ async close() {
842
+ await this.#watcher?.close();
843
+ this.#assetsServer?.stop();
844
+ if (this.#testScript) {
845
+ this.#testScript.removeAllListeners();
846
+ this.#testScript.kill("SIGKILL");
847
+ }
848
+ }
849
+ /**
850
+ * Runs tests
851
+ */
852
+ async run() {
853
+ const port = String(await getPort(this.#cwd));
854
+ this.#clearScreen();
855
+ this.#startAssetsServer();
856
+ this.#logger.info("booting application to run tests...");
857
+ this.#runTests(port, "nonblocking");
858
+ }
859
+ /**
860
+ * Run tests in watch mode
861
+ */
862
+ async runAndWatch(ts, options) {
863
+ const port = String(await getPort(this.#cwd));
864
+ this.#clearScreen();
865
+ this.#startAssetsServer();
866
+ this.#logger.info("booting application to run tests...");
867
+ this.#runTests(port, "blocking");
868
+ const output = watch(this.#cwd, ts, options || {});
869
+ if (!output) {
870
+ this.#onClose?.(1);
871
+ return;
872
+ }
873
+ this.#watcher = output.chokidar;
874
+ output.watcher.on("watcher:ready", () => {
875
+ this.#logger.info("watching file system for changes...");
876
+ });
877
+ output.chokidar.on("error", (error) => {
878
+ this.#logger.warning("file system watcher failure");
879
+ this.#logger.fatal(error);
880
+ this.#onError?.(error);
881
+ output.chokidar.close();
882
+ });
883
+ output.watcher.on(
884
+ "source:add",
885
+ ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
886
+ );
887
+ output.watcher.on(
888
+ "source:change",
889
+ ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
890
+ );
891
+ output.watcher.on(
892
+ "source:unlink",
893
+ ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
894
+ );
895
+ output.watcher.on(
896
+ "add",
897
+ ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
898
+ );
899
+ output.watcher.on(
900
+ "change",
901
+ ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
902
+ );
903
+ output.watcher.on(
904
+ "unlink",
905
+ ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
906
+ );
907
+ }
908
+ };
909
+ export {
910
+ Bundler,
911
+ DevServer,
912
+ TestRunner
913
+ };