@adonisjs/assembler 6.1.3-23 → 6.1.3-25

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,920 @@
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
+ import prettyHrtime from "pretty-hrtime";
414
+ var ui3 = cliui3();
415
+ var DevServer = class {
416
+ #cwd;
417
+ #logger = ui3.logger;
418
+ #options;
419
+ #isWatching = false;
420
+ #scriptFile = "bin/server.js";
421
+ #isMetaFileWithReloadsEnabled;
422
+ #isMetaFileWithReloadsDisabled;
423
+ #onError;
424
+ #onClose;
425
+ #httpServer;
426
+ #watcher;
427
+ #assetsServer;
428
+ /**
429
+ * Getting reference to colors library from logger
430
+ */
431
+ get #colors() {
432
+ return this.#logger.getColors();
433
+ }
434
+ constructor(cwd, options) {
435
+ this.#cwd = cwd;
436
+ this.#options = options;
437
+ this.#isMetaFileWithReloadsEnabled = picomatch(
438
+ (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer === true).map(({ pattern }) => pattern)
439
+ );
440
+ this.#isMetaFileWithReloadsDisabled = picomatch(
441
+ (this.#options.metaFiles || []).filter(({ reloadServer }) => reloadServer !== true).map(({ pattern }) => pattern)
442
+ );
443
+ }
444
+ /**
445
+ * Inspect if child process message is from AdonisJS HTTP server
446
+ */
447
+ #isAdonisJSReadyMessage(message) {
448
+ return message !== null && typeof message === "object" && "isAdonisJS" in message && "environment" in message && message.environment === "web";
449
+ }
450
+ /**
451
+ * Conditionally clear the terminal screen
452
+ */
453
+ #clearScreen() {
454
+ if (this.#options.clearScreen) {
455
+ process.stdout.write("\x1Bc");
456
+ }
457
+ }
458
+ /**
459
+ * Starts the HTTP server
460
+ */
461
+ #startHTTPServer(port, mode) {
462
+ let initialTime = process.hrtime();
463
+ this.#httpServer = runNode(this.#cwd, {
464
+ script: this.#scriptFile,
465
+ env: { PORT: port, ...this.#options.env },
466
+ nodeArgs: this.#options.nodeArgs,
467
+ scriptArgs: this.#options.scriptArgs
468
+ });
469
+ this.#httpServer.on("message", (message) => {
470
+ if (this.#isAdonisJSReadyMessage(message)) {
471
+ const readyAt = process.hrtime(initialTime);
472
+ ui3.sticker().useColors(this.#colors).useRenderer(this.#logger.getRenderer()).add(`Server address: ${this.#colors.cyan(`http://${message.host}:${message.port}`)}`).add(
473
+ `File system watcher: ${this.#colors.cyan(
474
+ `${this.#isWatching ? "enabled" : "disabled"}`
475
+ )}`
476
+ ).add(`Ready in: ${this.#colors.cyan(prettyHrtime(readyAt))}`).render();
477
+ }
478
+ });
479
+ this.#httpServer.then((result) => {
480
+ if (mode === "nonblocking") {
481
+ this.#onClose?.(result.exitCode);
482
+ this.#watcher?.close();
483
+ this.#assetsServer?.stop();
484
+ } else {
485
+ this.#logger.info("Underlying HTTP server closed. Still watching for changes");
486
+ }
487
+ }).catch((error) => {
488
+ if (mode === "nonblocking") {
489
+ this.#onError?.(error);
490
+ this.#watcher?.close();
491
+ this.#assetsServer?.stop();
492
+ } else {
493
+ this.#logger.info("Underlying HTTP server died. Still watching for changes");
494
+ }
495
+ });
496
+ }
497
+ /**
498
+ * Starts the assets server
499
+ */
500
+ #startAssetsServer() {
501
+ this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
502
+ this.#assetsServer.setLogger(this.#logger);
503
+ this.#assetsServer.start();
504
+ }
505
+ /**
506
+ * Restarts the HTTP server
507
+ */
508
+ #restartHTTPServer(port) {
509
+ if (this.#httpServer) {
510
+ this.#httpServer.removeAllListeners();
511
+ this.#httpServer.kill("SIGKILL");
512
+ }
513
+ this.#startHTTPServer(port, "blocking");
514
+ }
515
+ /**
516
+ * Handles a non TypeScript file change
517
+ */
518
+ #handleFileChange(action, port, relativePath) {
519
+ if (isDotEnvFile(relativePath) || isRcFile(relativePath)) {
520
+ this.#clearScreen();
521
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
522
+ this.#restartHTTPServer(port);
523
+ return;
524
+ }
525
+ if (this.#isMetaFileWithReloadsEnabled(relativePath)) {
526
+ this.#clearScreen();
527
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
528
+ this.#restartHTTPServer(port);
529
+ return;
530
+ }
531
+ if (this.#isMetaFileWithReloadsDisabled(relativePath)) {
532
+ this.#clearScreen();
533
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
534
+ }
535
+ }
536
+ /**
537
+ * Handles TypeScript source file change
538
+ */
539
+ #handleSourceFileChange(action, port, relativePath) {
540
+ this.#clearScreen();
541
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
542
+ this.#restartHTTPServer(port);
543
+ }
544
+ /**
545
+ * Set a custom CLI UI logger
546
+ */
547
+ setLogger(logger) {
548
+ this.#logger = logger;
549
+ this.#assetsServer?.setLogger(logger);
550
+ return this;
551
+ }
552
+ /**
553
+ * Add listener to get notified when dev server is
554
+ * closed
555
+ */
556
+ onClose(callback) {
557
+ this.#onClose = callback;
558
+ return this;
559
+ }
560
+ /**
561
+ * Add listener to get notified when dev server exists
562
+ * with an error
563
+ */
564
+ onError(callback) {
565
+ this.#onError = callback;
566
+ return this;
567
+ }
568
+ /**
569
+ * Close watchers and running child processes
570
+ */
571
+ async close() {
572
+ await this.#watcher?.close();
573
+ this.#assetsServer?.stop();
574
+ if (this.#httpServer) {
575
+ this.#httpServer.removeAllListeners();
576
+ this.#httpServer.kill("SIGKILL");
577
+ }
578
+ }
579
+ /**
580
+ * Start the development server
581
+ */
582
+ async start() {
583
+ this.#clearScreen();
584
+ this.#logger.info("starting HTTP server...");
585
+ this.#startHTTPServer(String(await getPort(this.#cwd)), "nonblocking");
586
+ this.#startAssetsServer();
587
+ }
588
+ /**
589
+ * Start the development server in watch mode
590
+ */
591
+ async startAndWatch(ts, options) {
592
+ const port = String(await getPort(this.#cwd));
593
+ this.#isWatching = true;
594
+ this.#clearScreen();
595
+ this.#logger.info("starting HTTP server...");
596
+ this.#startHTTPServer(port, "blocking");
597
+ this.#startAssetsServer();
598
+ const output = watch(this.#cwd, ts, options || {});
599
+ if (!output) {
600
+ this.#onClose?.(1);
601
+ return;
602
+ }
603
+ this.#watcher = output.chokidar;
604
+ output.watcher.on("watcher:ready", () => {
605
+ this.#logger.info("watching file system for changes...");
606
+ });
607
+ output.chokidar.on("error", (error) => {
608
+ this.#logger.warning("file system watcher failure");
609
+ this.#logger.fatal(error);
610
+ this.#onError?.(error);
611
+ output.chokidar.close();
612
+ });
613
+ output.watcher.on(
614
+ "source:add",
615
+ ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
616
+ );
617
+ output.watcher.on(
618
+ "source:change",
619
+ ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
620
+ );
621
+ output.watcher.on(
622
+ "source:unlink",
623
+ ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
624
+ );
625
+ output.watcher.on(
626
+ "add",
627
+ ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
628
+ );
629
+ output.watcher.on(
630
+ "change",
631
+ ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
632
+ );
633
+ output.watcher.on(
634
+ "unlink",
635
+ ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
636
+ );
637
+ }
638
+ };
639
+
640
+ // src/test_runner.ts
641
+ import picomatch2 from "picomatch";
642
+ import { cliui as cliui4 } from "@poppinss/cliui";
643
+ var ui4 = cliui4();
644
+ var TestRunner = class {
645
+ #cwd;
646
+ #logger = ui4.logger;
647
+ #options;
648
+ #scriptFile = "bin/test.js";
649
+ #isMetaFile;
650
+ #isTestFile;
651
+ #scriptArgs;
652
+ #initialFiltersArgs;
653
+ /**
654
+ * In watch mode, after a file is changed, we wait for the current
655
+ * set of tests to finish before triggering a re-run. Therefore,
656
+ * we use this flag to know if we are already busy in running
657
+ * tests and ignore file-changes.
658
+ */
659
+ #isBusy = false;
660
+ #onError;
661
+ #onClose;
662
+ #testScript;
663
+ #watcher;
664
+ #assetsServer;
665
+ /**
666
+ * Getting reference to colors library from logger
667
+ */
668
+ get #colors() {
669
+ return this.#logger.getColors();
670
+ }
671
+ constructor(cwd, options) {
672
+ this.#cwd = cwd;
673
+ this.#options = options;
674
+ this.#isMetaFile = picomatch2((this.#options.metaFiles || []).map(({ pattern }) => pattern));
675
+ this.#isTestFile = picomatch2(
676
+ this.#options.suites.filter((suite) => {
677
+ if (this.#options.filters.suites) {
678
+ return this.#options.filters.suites.includes(suite.name);
679
+ }
680
+ return true;
681
+ }).map((suite) => suite.files).flat(1)
682
+ );
683
+ this.#scriptArgs = this.#convertOptionsToArgs().concat(this.#options.scriptArgs);
684
+ this.#initialFiltersArgs = this.#convertFiltersToArgs(this.#options.filters);
685
+ }
686
+ /**
687
+ * Converts options to CLI args
688
+ */
689
+ #convertOptionsToArgs() {
690
+ const args = [];
691
+ if (this.#options.reporters) {
692
+ args.push("--reporters");
693
+ args.push(this.#options.reporters.join(","));
694
+ }
695
+ if (this.#options.timeout !== void 0) {
696
+ args.push("--timeout");
697
+ args.push(String(this.#options.timeout));
698
+ }
699
+ if (this.#options.failed) {
700
+ args.push("--failed");
701
+ }
702
+ if (this.#options.retries !== void 0) {
703
+ args.push("--retries");
704
+ args.push(String(this.#options.retries));
705
+ }
706
+ return args;
707
+ }
708
+ /**
709
+ * Converts all known filters to CLI args.
710
+ *
711
+ * The following code snippet may seem like repetitive code. But, it
712
+ * is done intentionally to have visibility around how each filter
713
+ * is converted to an arg.
714
+ */
715
+ #convertFiltersToArgs(filters) {
716
+ const args = [];
717
+ if (filters.suites) {
718
+ args.push(...filters.suites);
719
+ }
720
+ if (filters.files) {
721
+ args.push("--files");
722
+ args.push(filters.files.join(","));
723
+ }
724
+ if (filters.groups) {
725
+ args.push("--groups");
726
+ args.push(filters.groups.join(","));
727
+ }
728
+ if (filters.tags) {
729
+ args.push("--tags");
730
+ args.push(filters.tags.join(","));
731
+ }
732
+ if (filters.tests) {
733
+ args.push("--tests");
734
+ args.push(filters.tests.join(","));
735
+ }
736
+ return args;
737
+ }
738
+ /**
739
+ * Conditionally clear the terminal screen
740
+ */
741
+ #clearScreen() {
742
+ if (this.#options.clearScreen) {
743
+ process.stdout.write("\x1Bc");
744
+ }
745
+ }
746
+ /**
747
+ * Runs tests
748
+ */
749
+ #runTests(port, mode, filters) {
750
+ this.#isBusy = true;
751
+ const scriptArgs = filters ? this.#convertFiltersToArgs(filters).concat(this.#scriptArgs) : this.#initialFiltersArgs.concat(this.#scriptArgs);
752
+ this.#testScript = runNode(this.#cwd, {
753
+ script: this.#scriptFile,
754
+ env: { PORT: port, ...this.#options.env },
755
+ nodeArgs: this.#options.nodeArgs,
756
+ scriptArgs
757
+ });
758
+ this.#testScript.then((result) => {
759
+ if (mode === "nonblocking") {
760
+ this.#onClose?.(result.exitCode);
761
+ this.close();
762
+ }
763
+ }).catch((error) => {
764
+ if (mode === "nonblocking") {
765
+ this.#onError?.(error);
766
+ this.close();
767
+ }
768
+ }).finally(() => {
769
+ this.#isBusy = false;
770
+ });
771
+ }
772
+ /**
773
+ * Restarts the HTTP server
774
+ */
775
+ #rerunTests(port, filters) {
776
+ if (this.#testScript) {
777
+ this.#testScript.removeAllListeners();
778
+ this.#testScript.kill("SIGKILL");
779
+ }
780
+ this.#runTests(port, "blocking", filters);
781
+ }
782
+ /**
783
+ * Starts the assets server
784
+ */
785
+ #startAssetsServer() {
786
+ this.#assetsServer = new AssetsDevServer(this.#cwd, this.#options.assets);
787
+ this.#assetsServer.setLogger(this.#logger);
788
+ this.#assetsServer.start();
789
+ }
790
+ /**
791
+ * Handles a non TypeScript file change
792
+ */
793
+ #handleFileChange(action, port, relativePath) {
794
+ if (this.#isBusy) {
795
+ return;
796
+ }
797
+ if (isDotEnvFile(relativePath) || this.#isMetaFile(relativePath)) {
798
+ this.#clearScreen();
799
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
800
+ this.#rerunTests(port);
801
+ }
802
+ }
803
+ /**
804
+ * Handles TypeScript source file change
805
+ */
806
+ #handleSourceFileChange(action, port, relativePath) {
807
+ if (this.#isBusy) {
808
+ return;
809
+ }
810
+ this.#clearScreen();
811
+ this.#logger.log(`${this.#colors.green(action)} ${relativePath}`);
812
+ if (this.#isTestFile(relativePath)) {
813
+ this.#rerunTests(port, {
814
+ ...this.#options.filters,
815
+ files: [relativePath]
816
+ });
817
+ return;
818
+ }
819
+ this.#rerunTests(port);
820
+ }
821
+ /**
822
+ * Set a custom CLI UI logger
823
+ */
824
+ setLogger(logger) {
825
+ this.#logger = logger;
826
+ this.#assetsServer?.setLogger(logger);
827
+ return this;
828
+ }
829
+ /**
830
+ * Add listener to get notified when dev server is
831
+ * closed
832
+ */
833
+ onClose(callback) {
834
+ this.#onClose = callback;
835
+ return this;
836
+ }
837
+ /**
838
+ * Add listener to get notified when dev server exists
839
+ * with an error
840
+ */
841
+ onError(callback) {
842
+ this.#onError = callback;
843
+ return this;
844
+ }
845
+ /**
846
+ * Close watchers and running child processes
847
+ */
848
+ async close() {
849
+ await this.#watcher?.close();
850
+ this.#assetsServer?.stop();
851
+ if (this.#testScript) {
852
+ this.#testScript.removeAllListeners();
853
+ this.#testScript.kill("SIGKILL");
854
+ }
855
+ }
856
+ /**
857
+ * Runs tests
858
+ */
859
+ async run() {
860
+ const port = String(await getPort(this.#cwd));
861
+ this.#clearScreen();
862
+ this.#startAssetsServer();
863
+ this.#logger.info("booting application to run tests...");
864
+ this.#runTests(port, "nonblocking");
865
+ }
866
+ /**
867
+ * Run tests in watch mode
868
+ */
869
+ async runAndWatch(ts, options) {
870
+ const port = String(await getPort(this.#cwd));
871
+ this.#clearScreen();
872
+ this.#startAssetsServer();
873
+ this.#logger.info("booting application to run tests...");
874
+ this.#runTests(port, "blocking");
875
+ const output = watch(this.#cwd, ts, options || {});
876
+ if (!output) {
877
+ this.#onClose?.(1);
878
+ return;
879
+ }
880
+ this.#watcher = output.chokidar;
881
+ output.watcher.on("watcher:ready", () => {
882
+ this.#logger.info("watching file system for changes...");
883
+ });
884
+ output.chokidar.on("error", (error) => {
885
+ this.#logger.warning("file system watcher failure");
886
+ this.#logger.fatal(error);
887
+ this.#onError?.(error);
888
+ output.chokidar.close();
889
+ });
890
+ output.watcher.on(
891
+ "source:add",
892
+ ({ relativePath }) => this.#handleSourceFileChange("add", port, relativePath)
893
+ );
894
+ output.watcher.on(
895
+ "source:change",
896
+ ({ relativePath }) => this.#handleSourceFileChange("update", port, relativePath)
897
+ );
898
+ output.watcher.on(
899
+ "source:unlink",
900
+ ({ relativePath }) => this.#handleSourceFileChange("delete", port, relativePath)
901
+ );
902
+ output.watcher.on(
903
+ "add",
904
+ ({ relativePath }) => this.#handleFileChange("add", port, relativePath)
905
+ );
906
+ output.watcher.on(
907
+ "change",
908
+ ({ relativePath }) => this.#handleFileChange("update", port, relativePath)
909
+ );
910
+ output.watcher.on(
911
+ "unlink",
912
+ ({ relativePath }) => this.#handleFileChange("delete", port, relativePath)
913
+ );
914
+ }
915
+ };
916
+ export {
917
+ Bundler,
918
+ DevServer,
919
+ TestRunner
920
+ };