@rstest/browser 0.9.0 → 0.9.2

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.
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import type { IncomingMessage, ServerResponse } from 'node:http';
4
4
  import type { AddressInfo } from 'node:net';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import type { Rspack } from '@rstest/core';
6
7
  import {
7
8
  type BrowserTestRunOptions,
8
9
  type BrowserTestRunResult,
@@ -37,6 +38,7 @@ import {
37
38
  createHostDispatchRouter,
38
39
  type HostDispatchRouterOptions,
39
40
  } from './dispatchCapabilities';
41
+ import { createHeadedSerialTaskQueue } from './headedSerialTaskQueue';
40
42
  import { createHeadlessLatestRerunScheduler } from './headlessLatestRerunScheduler';
41
43
  import { attachHeadlessRunnerTransport } from './headlessTransport';
42
44
  import type {
@@ -76,6 +78,11 @@ import {
76
78
  type SourceMapPayload,
77
79
  } from './sourceMap/sourceMapLoader';
78
80
  import { resolveBrowserViewportPreset } from './viewportPresets';
81
+ import {
82
+ isBrowserWatchCliShortcutsEnabled,
83
+ logBrowserWatchReadyMessage,
84
+ setupBrowserWatchCliShortcuts,
85
+ } from './watchCliShortcuts';
79
86
  import { collectWatchTestFiles, planWatchRerun } from './watchRerunPlanner';
80
87
 
81
88
  const { createRsbuild, rspack } = rsbuild;
@@ -155,6 +162,7 @@ type TestCaseStartPayload = ReporterHookArg<'onTestCaseStart'>;
155
162
  type HostRpcMethods = {
156
163
  rerunTest: (testFile: string, testNamePattern?: string) => Promise<void>;
157
164
  getTestFiles: () => Promise<TestFileInfo[]>;
165
+ onRunnerFramesReady: (testFiles: string[]) => Promise<void>;
158
166
  // Test result callbacks from container
159
167
  onTestFileStart: (payload: TestFileStartPayload) => Promise<void>;
160
168
  onTestCaseResult: (payload: TestResult) => Promise<void>;
@@ -314,6 +322,8 @@ type WatchContext = {
314
322
  lastTestFiles: TestFileInfo[];
315
323
  hooksEnabled: boolean;
316
324
  cleanupRegistered: boolean;
325
+ cleanupPromise: Promise<void> | null;
326
+ closeCliShortcuts: (() => void) | null;
317
327
  chunkHashes: Map<string, string>;
318
328
  affectedTestFiles: string[];
319
329
  };
@@ -323,6 +333,8 @@ const watchContext: WatchContext = {
323
333
  lastTestFiles: [],
324
334
  hooksEnabled: false,
325
335
  cleanupRegistered: false,
336
+ cleanupPromise: null,
337
+ closeCliShortcuts: null,
326
338
  chunkHashes: new Map(),
327
339
  affectedTestFiles: [],
328
340
  };
@@ -378,6 +390,81 @@ const ensureProcessExitCode = (code: number): void => {
378
390
  }
379
391
  };
380
392
 
393
+ const castArray = <T>(arr?: T | T[]): T[] => {
394
+ if (arr === undefined) {
395
+ return [];
396
+ }
397
+ return Array.isArray(arr) ? arr : [arr];
398
+ };
399
+
400
+ const applyDefaultWatchOptions = (
401
+ rspackConfig: Rspack.Configuration,
402
+ isWatchMode: boolean,
403
+ ) => {
404
+ rspackConfig.watchOptions ??= {};
405
+
406
+ if (!isWatchMode) {
407
+ rspackConfig.watchOptions.ignored = '**/**';
408
+ return;
409
+ }
410
+
411
+ rspackConfig.watchOptions.ignored = castArray(
412
+ rspackConfig.watchOptions.ignored || [],
413
+ ) as string[];
414
+
415
+ if (rspackConfig.watchOptions.ignored.length === 0) {
416
+ rspackConfig.watchOptions.ignored.push('**/.git', '**/node_modules');
417
+ }
418
+
419
+ rspackConfig.output?.path &&
420
+ rspackConfig.watchOptions.ignored.push(rspackConfig.output.path);
421
+ };
422
+
423
+ type LazyCompilationModule = {
424
+ nameForCondition?: () => string | null | undefined;
425
+ };
426
+
427
+ type BrowserLazyCompilationConfig = {
428
+ imports: true;
429
+ entries: false;
430
+ test?: (module: LazyCompilationModule) => boolean;
431
+ };
432
+
433
+ export const createBrowserLazyCompilationConfig = (
434
+ setupFiles: string[],
435
+ ): BrowserLazyCompilationConfig => {
436
+ const eagerSetupFiles = new Set(
437
+ setupFiles.map((filePath) => normalize(filePath)),
438
+ );
439
+
440
+ if (eagerSetupFiles.size === 0) {
441
+ return {
442
+ imports: true,
443
+ entries: false,
444
+ };
445
+ }
446
+
447
+ return {
448
+ imports: true,
449
+ entries: false,
450
+ test(module: LazyCompilationModule) {
451
+ const filePath = module.nameForCondition?.();
452
+ return !filePath || !eagerSetupFiles.has(normalize(filePath));
453
+ },
454
+ };
455
+ };
456
+
457
+ export const createBrowserRsbuildDevConfig = (isWatchMode: boolean) => {
458
+ return {
459
+ // Disable HMR in non-watch mode (tests run once and exit).
460
+ // Aligns with node mode behavior (packages/core/src/core/rsbuild.ts).
461
+ hmr: isWatchMode,
462
+ client: {
463
+ logLevel: 'error' as const,
464
+ },
465
+ };
466
+ };
467
+
381
468
  /**
382
469
  * Convert a single glob pattern to RegExp using picomatch
383
470
  * Based on Storybook's implementation
@@ -920,27 +1007,39 @@ const destroyBrowserRuntime = async (
920
1007
  .catch(() => {});
921
1008
  };
922
1009
 
923
- const registerWatchCleanup = (): void => {
924
- if (watchContext.cleanupRegistered) {
925
- return;
1010
+ const cleanupWatchRuntime = (): Promise<void> => {
1011
+ if (watchContext.cleanupPromise) {
1012
+ return watchContext.cleanupPromise;
926
1013
  }
927
1014
 
928
- const cleanup = async () => {
1015
+ watchContext.cleanupPromise = (async () => {
1016
+ watchContext.closeCliShortcuts?.();
1017
+ watchContext.closeCliShortcuts = null;
1018
+
929
1019
  if (!watchContext.runtime) {
930
1020
  return;
931
1021
  }
1022
+
932
1023
  await destroyBrowserRuntime(watchContext.runtime);
933
1024
  watchContext.runtime = null;
934
- };
1025
+ })();
1026
+
1027
+ return watchContext.cleanupPromise;
1028
+ };
1029
+
1030
+ const registerWatchCleanup = (): void => {
1031
+ if (watchContext.cleanupRegistered) {
1032
+ return;
1033
+ }
935
1034
 
936
- for (const signal of ['SIGINT', 'SIGTERM'] as const) {
1035
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGTSTP'] as const) {
937
1036
  process.once(signal, () => {
938
- void cleanup();
1037
+ void cleanupWatchRuntime();
939
1038
  });
940
1039
  }
941
1040
 
942
1041
  process.once('exit', () => {
943
- void cleanup();
1042
+ void cleanupWatchRuntime();
944
1043
  });
945
1044
 
946
1045
  watchContext.cleanupRegistered = true;
@@ -1029,11 +1128,7 @@ const createBrowserRuntime = async ({
1029
1128
  port: browserLaunchOptions.port ?? 4000,
1030
1129
  strictPort: browserLaunchOptions.strictPort,
1031
1130
  },
1032
- dev: {
1033
- client: {
1034
- logLevel: 'error',
1035
- },
1036
- },
1131
+ dev: createBrowserRsbuildDevConfig(isWatchMode),
1037
1132
  environments: {
1038
1133
  ...Object.fromEntries(
1039
1134
  browserProjects.map((project) => [project.environmentName, {}]),
@@ -1069,6 +1164,12 @@ const createBrowserRuntime = async ({
1069
1164
  }
1070
1165
 
1071
1166
  const userRsbuildConfig = project.normalizedConfig;
1167
+ const setupFiles = Object.values(
1168
+ getSetupFiles(
1169
+ project.normalizedConfig.setupFiles,
1170
+ project.rootPath,
1171
+ ),
1172
+ );
1072
1173
  // Merge order: current config -> userConfig -> rstest required config (highest priority)
1073
1174
  const merged = mergeEnvironmentConfig(config, userRsbuildConfig, {
1074
1175
  resolve: {
@@ -1090,13 +1191,13 @@ const createBrowserRuntime = async ({
1090
1191
  tools: {
1091
1192
  rspack: (rspackConfig) => {
1092
1193
  rspackConfig.mode = 'development';
1093
- rspackConfig.lazyCompilation = {
1094
- imports: true,
1095
- entries: false,
1096
- };
1194
+ rspackConfig.lazyCompilation =
1195
+ createBrowserLazyCompilationConfig(setupFiles);
1097
1196
  rspackConfig.plugins = rspackConfig.plugins || [];
1098
1197
  rspackConfig.plugins.push(virtualManifestPlugin);
1099
1198
 
1199
+ applyDefaultWatchOptions(rspackConfig, isWatchMode);
1200
+
1100
1201
  // Extract and merge sourcemaps from pre-built @rstest/core files
1101
1202
  // This preserves the sourcemap chain for inline snapshot support
1102
1203
  // See: https://rspack.dev/config/module-rules#rulesextractsourcemap
@@ -1638,6 +1739,7 @@ export const runBrowserController = async (
1638
1739
  await notifyTestRunStart();
1639
1740
 
1640
1741
  const isWatchMode = context.command === 'watch';
1742
+ const enableCliShortcuts = isWatchMode && isBrowserWatchCliShortcutsEnabled();
1641
1743
  const tempDir =
1642
1744
  isWatchMode && watchContext.runtime
1643
1745
  ? watchContext.runtime.tempDir
@@ -1691,6 +1793,12 @@ export const runBrowserController = async (
1691
1793
  if (isWatchMode) {
1692
1794
  watchContext.runtime = runtime;
1693
1795
  registerWatchCleanup();
1796
+
1797
+ if (enableCliShortcuts && !watchContext.closeCliShortcuts) {
1798
+ watchContext.closeCliShortcuts = await setupBrowserWatchCliShortcuts({
1799
+ close: cleanupWatchRuntime,
1800
+ });
1801
+ }
1694
1802
  }
1695
1803
  }
1696
1804
 
@@ -2331,6 +2439,7 @@ export const runBrowserController = async (
2331
2439
  ? [rerunFatalError]
2332
2440
  : undefined,
2333
2441
  });
2442
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2334
2443
  }
2335
2444
  },
2336
2445
  onError: async (error) => {
@@ -2375,6 +2484,7 @@ export const runBrowserController = async (
2375
2484
  logger.log(
2376
2485
  color.cyan('No browser test files remain after update.\n'),
2377
2486
  );
2487
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2378
2488
  return;
2379
2489
  }
2380
2490
 
@@ -2393,6 +2503,7 @@ export const runBrowserController = async (
2393
2503
  'No affected browser test files detected, skipping re-run.\n',
2394
2504
  ),
2395
2505
  );
2506
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2396
2507
  return;
2397
2508
  }
2398
2509
 
@@ -2451,23 +2562,104 @@ export const runBrowserController = async (
2451
2562
 
2452
2563
  if (isWatchMode && triggerRerun) {
2453
2564
  watchContext.hooksEnabled = true;
2454
- logger.log(
2455
- color.cyan(
2456
- '\nWatch mode enabled - will re-run tests on file changes\n',
2457
- ),
2458
- );
2565
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2459
2566
  }
2460
2567
 
2461
2568
  return result;
2462
2569
  }
2463
2570
 
2464
- let completedTests = 0;
2571
+ let currentTestFiles = allTestFiles;
2572
+ const RUNNER_FRAMES_READY_TIMEOUT_MS = 30_000;
2573
+ let currentRunnerFramesSignature: string | null = null;
2574
+ const runnerFramesWaiters = new Map<string, Set<() => void>>();
2465
2575
 
2466
- // Promise that resolves when all tests complete
2467
- let resolveAllTests: (() => void) | undefined;
2468
- const allTestsPromise = new Promise<void>((resolve) => {
2469
- resolveAllTests = resolve;
2470
- });
2576
+ const createTestFilesSignature = (testFiles: readonly string[]): string => {
2577
+ return JSON.stringify(testFiles.map((testFile) => normalize(testFile)));
2578
+ };
2579
+
2580
+ const markRunnerFramesReady = (testFiles: string[]): void => {
2581
+ const signature = createTestFilesSignature(testFiles);
2582
+ currentRunnerFramesSignature = signature;
2583
+ const waiters = runnerFramesWaiters.get(signature);
2584
+ if (!waiters) {
2585
+ return;
2586
+ }
2587
+ runnerFramesWaiters.delete(signature);
2588
+ for (const waiter of waiters) {
2589
+ waiter();
2590
+ }
2591
+ };
2592
+
2593
+ const waitForRunnerFramesReady = async (
2594
+ testFiles: readonly string[],
2595
+ ): Promise<void> => {
2596
+ const signature = createTestFilesSignature(testFiles);
2597
+ if (currentRunnerFramesSignature === signature) {
2598
+ return;
2599
+ }
2600
+
2601
+ await new Promise<void>((resolve, reject) => {
2602
+ const waiters =
2603
+ runnerFramesWaiters.get(signature) ?? new Set<() => void>();
2604
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
2605
+
2606
+ const cleanup = () => {
2607
+ const currentWaiters = runnerFramesWaiters.get(signature);
2608
+ if (!currentWaiters) {
2609
+ return;
2610
+ }
2611
+ currentWaiters.delete(onReady);
2612
+ if (currentWaiters.size === 0) {
2613
+ runnerFramesWaiters.delete(signature);
2614
+ }
2615
+ };
2616
+
2617
+ const onReady = () => {
2618
+ if (timeoutId) {
2619
+ clearTimeout(timeoutId);
2620
+ }
2621
+ cleanup();
2622
+ resolve();
2623
+ };
2624
+
2625
+ timeoutId = setTimeout(() => {
2626
+ cleanup();
2627
+ reject(
2628
+ new Error(
2629
+ `Timed out waiting for headed runner frames to be ready for ${testFiles.length} file(s).`,
2630
+ ),
2631
+ );
2632
+ }, RUNNER_FRAMES_READY_TIMEOUT_MS);
2633
+
2634
+ waiters.add(onReady);
2635
+ runnerFramesWaiters.set(signature, waiters);
2636
+
2637
+ if (currentRunnerFramesSignature === signature) {
2638
+ onReady();
2639
+ }
2640
+ });
2641
+ };
2642
+
2643
+ const getTestFileInfo = (testFile: string): TestFileInfo => {
2644
+ const normalizedTestFile = normalize(testFile);
2645
+ const fileInfo = currentTestFiles.find(
2646
+ (file) => file.testPath === normalizedTestFile,
2647
+ );
2648
+ if (!fileInfo) {
2649
+ throw new Error(`Unknown browser test file: ${JSON.stringify(testFile)}`);
2650
+ }
2651
+ return fileInfo;
2652
+ };
2653
+
2654
+ const getHeadedPerFileTimeoutMs = (file: TestFileInfo): number => {
2655
+ const projectRuntime = projectRuntimeConfigs.find(
2656
+ (project) => project.name === file.projectName,
2657
+ );
2658
+ return (
2659
+ (projectRuntime?.runtimeConfig.testTimeout ?? maxTestTimeoutForRpc) +
2660
+ 30_000
2661
+ );
2662
+ };
2471
2663
 
2472
2664
  // Open a container page for user to view (reuse in watch mode)
2473
2665
  let containerContext: BrowserProviderContext;
@@ -2513,6 +2705,40 @@ export const runBrowserController = async (
2513
2705
  activeContainerPage = containerPage;
2514
2706
 
2515
2707
  const dispatchRouter = createDispatchRouter();
2708
+ const headedReloadQueue = createHeadedSerialTaskQueue();
2709
+ let enqueueHeadedReload = async (
2710
+ _file: TestFileInfo,
2711
+ _testNamePattern?: string,
2712
+ ): Promise<void> => {
2713
+ throw new Error('Headed reload queue is not initialized');
2714
+ };
2715
+
2716
+ const reloadTestFileWithTimeout = async (
2717
+ file: TestFileInfo,
2718
+ testNamePattern?: string,
2719
+ ): Promise<void> => {
2720
+ const timeoutMs = getHeadedPerFileTimeoutMs(file);
2721
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
2722
+
2723
+ try {
2724
+ await Promise.race([
2725
+ rpcManager.reloadTestFile(file.testPath, testNamePattern),
2726
+ new Promise<never>((_, reject) => {
2727
+ timeoutId = setTimeout(() => {
2728
+ reject(
2729
+ new Error(
2730
+ `Headed test execution timeout after ${timeoutMs / 1000}s for ${file.testPath}.`,
2731
+ ),
2732
+ );
2733
+ }, timeoutMs);
2734
+ }),
2735
+ ]);
2736
+ } finally {
2737
+ if (timeoutId) {
2738
+ clearTimeout(timeoutId);
2739
+ }
2740
+ }
2741
+ };
2516
2742
 
2517
2743
  // Create RPC methods that can access test state variables
2518
2744
  const createRpcMethods = (): HostRpcMethods => ({
@@ -2525,10 +2751,13 @@ export const runBrowserController = async (
2525
2751
  `\nRe-running test: ${displayPath}${testNamePattern ? ` (pattern: ${testNamePattern})` : ''}\n`,
2526
2752
  ),
2527
2753
  );
2528
- await rpcManager.reloadTestFile(testFile, testNamePattern);
2754
+ await enqueueHeadedReload(getTestFileInfo(testFile), testNamePattern);
2529
2755
  },
2530
2756
  async getTestFiles() {
2531
- return allTestFiles;
2757
+ return currentTestFiles;
2758
+ },
2759
+ async onRunnerFramesReady(testFiles: string[]) {
2760
+ markRunnerFramesReady(testFiles);
2532
2761
  },
2533
2762
  async onTestFileStart(payload: TestFileStartPayload) {
2534
2763
  await handleTestFileStart(payload);
@@ -2538,20 +2767,12 @@ export const runBrowserController = async (
2538
2767
  },
2539
2768
  async onTestFileComplete(payload: TestFileResult) {
2540
2769
  await handleTestFileComplete(payload);
2541
-
2542
- completedTests++;
2543
- if (completedTests >= allTestFiles.length && resolveAllTests) {
2544
- resolveAllTests();
2545
- }
2546
2770
  },
2547
2771
  async onLog(payload: LogPayload) {
2548
2772
  await handleLog(payload);
2549
2773
  },
2550
2774
  async onFatal(payload: FatalPayload) {
2551
2775
  await handleFatal(payload);
2552
- if (resolveAllTests) {
2553
- resolveAllTests();
2554
- }
2555
2776
  },
2556
2777
  async dispatch(request: BrowserDispatchRequest) {
2557
2778
  // Headed/container path now shares the same dispatch contract as headless.
@@ -2593,31 +2814,33 @@ export const runBrowserController = async (
2593
2814
  );
2594
2815
  }
2595
2816
 
2596
- // Wait for all tests to complete
2597
- // Calculate total timeout based on config: max testTimeout * file count + buffer
2598
- const maxTestTimeout = Math.max(
2599
- ...browserProjects.map((p) => p.normalizedConfig.testTimeout ?? 5000),
2600
- );
2601
- const totalTimeoutMs = maxTestTimeout * allTestFiles.length + 30_000;
2602
-
2603
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
2604
- const testTimeout = new Promise<void>((resolve) => {
2605
- timeoutId = setTimeout(() => {
2606
- logger.log(
2607
- color.yellow(
2608
- `\nTest execution timeout after ${totalTimeoutMs / 1000}s. ` +
2609
- `Completed: ${completedTests}/${allTestFiles.length}\n`,
2610
- ),
2611
- );
2612
- resolve();
2613
- }, totalTimeoutMs);
2614
- });
2817
+ enqueueHeadedReload = async (
2818
+ file: TestFileInfo,
2819
+ testNamePattern?: string,
2820
+ ): Promise<void> => {
2821
+ return headedReloadQueue.enqueue(async () => {
2822
+ if (fatalError) {
2823
+ return;
2824
+ }
2825
+ await reloadTestFileWithTimeout(file, testNamePattern);
2826
+ });
2827
+ };
2615
2828
 
2616
2829
  const testStart = Date.now();
2617
- await Promise.race([allTestsPromise, testTimeout]);
2830
+ try {
2831
+ await waitForRunnerFramesReady(
2832
+ currentTestFiles.map((file) => file.testPath),
2833
+ );
2618
2834
 
2619
- if (timeoutId) {
2620
- clearTimeout(timeoutId);
2835
+ for (const file of currentTestFiles) {
2836
+ await enqueueHeadedReload(file);
2837
+ if (fatalError) {
2838
+ break;
2839
+ }
2840
+ }
2841
+ } catch (error) {
2842
+ fatalError = fatalError ?? toError(error);
2843
+ ensureProcessExitCode(1);
2621
2844
  }
2622
2845
 
2623
2846
  const testTime = Date.now() - testStart;
@@ -2642,7 +2865,11 @@ export const runBrowserController = async (
2642
2865
  context.updateReporterResultState([], [], deletedTestPaths);
2643
2866
  }
2644
2867
  watchContext.lastTestFiles = rerunPlan.currentTestFiles;
2645
- await rpcManager.notifyTestFileUpdate(rerunPlan.currentTestFiles);
2868
+ currentTestFiles = rerunPlan.currentTestFiles;
2869
+ await rpcManager.notifyTestFileUpdate(currentTestFiles);
2870
+ await waitForRunnerFramesReady(
2871
+ currentTestFiles.map((file) => file.testPath),
2872
+ );
2646
2873
  }
2647
2874
 
2648
2875
  if (rerunPlan.normalizedAffectedTestFiles.length > 0) {
@@ -2659,7 +2886,7 @@ export const runBrowserController = async (
2659
2886
 
2660
2887
  try {
2661
2888
  for (const testFile of rerunPlan.normalizedAffectedTestFiles) {
2662
- await rpcManager.reloadTestFile(testFile);
2889
+ await enqueueHeadedReload(getTestFileInfo(testFile));
2663
2890
  }
2664
2891
  } catch (error) {
2665
2892
  rerunError = toError(error);
@@ -2683,9 +2910,13 @@ export const runBrowserController = async (
2683
2910
  ? [rerunFatalError]
2684
2911
  : undefined,
2685
2912
  });
2913
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2686
2914
  }
2687
2915
  } else if (!rerunPlan.filesChanged) {
2688
2916
  logger.log(color.cyan('Tests will be re-executed automatically\n'));
2917
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2918
+ } else {
2919
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2689
2920
  }
2690
2921
  };
2691
2922
  }
@@ -2746,9 +2977,7 @@ export const runBrowserController = async (
2746
2977
  // Enable watch hooks AFTER initial test run to avoid duplicate runs
2747
2978
  if (isWatchMode && triggerRerun) {
2748
2979
  watchContext.hooksEnabled = true;
2749
- logger.log(
2750
- color.cyan('\nWatch mode enabled - will re-run tests on file changes\n'),
2751
- );
2980
+ logBrowserWatchReadyMessage(enableCliShortcuts);
2752
2981
  }
2753
2982
 
2754
2983
  return result;
@@ -0,0 +1,77 @@
1
+ import { color, logger } from '@rstest/core/browser';
2
+
3
+ const isTTY = (): boolean => Boolean(process.stdin.isTTY && !process.env.CI);
4
+
5
+ export const isBrowserWatchCliShortcutsEnabled = (): boolean => isTTY();
6
+
7
+ export const getBrowserWatchCliShortcutsHintMessage = (): string => {
8
+ return ` ${color.dim('press')} ${color.bold('q')} ${color.dim('to quit')}\n`;
9
+ };
10
+
11
+ export const logBrowserWatchReadyMessage = (
12
+ enableCliShortcuts: boolean,
13
+ ): void => {
14
+ logger.log(color.green(' Waiting for file changes...'));
15
+
16
+ if (enableCliShortcuts) {
17
+ logger.log(getBrowserWatchCliShortcutsHintMessage());
18
+ }
19
+ };
20
+
21
+ export async function setupBrowserWatchCliShortcuts({
22
+ close,
23
+ }: {
24
+ close: () => Promise<void>;
25
+ }): Promise<() => void> {
26
+ const { emitKeypressEvents } = await import('node:readline');
27
+
28
+ emitKeypressEvents(process.stdin);
29
+ process.stdin.setRawMode(true);
30
+ process.stdin.resume();
31
+ process.stdin.setEncoding('utf8');
32
+
33
+ let isClosing = false;
34
+
35
+ const handleKeypress = (
36
+ str: string,
37
+ key: { name: string; ctrl: boolean },
38
+ ) => {
39
+ if (key.ctrl && key.name === 'c') {
40
+ process.kill(process.pid, 'SIGINT');
41
+ return;
42
+ }
43
+
44
+ if (key.ctrl && key.name === 'z') {
45
+ if (process.platform !== 'win32') {
46
+ process.kill(process.pid, 'SIGTSTP');
47
+ }
48
+ return;
49
+ }
50
+
51
+ if (str !== 'q' || isClosing) {
52
+ return;
53
+ }
54
+
55
+ // TODO: Support more browser watch shortcuts only after this path is
56
+ // refactored to share the same shortcut model as node mode.
57
+ isClosing = true;
58
+ void (async () => {
59
+ try {
60
+ await close();
61
+ } finally {
62
+ process.exit(0);
63
+ }
64
+ })();
65
+ };
66
+
67
+ process.stdin.on('keypress', handleKeypress);
68
+
69
+ return () => {
70
+ try {
71
+ process.stdin.setRawMode(false);
72
+ process.stdin.pause();
73
+ } catch {}
74
+
75
+ process.stdin.off('keypress', handleKeypress);
76
+ };
77
+ }
@@ -1 +0,0 @@
1
- /*! LICENSE: 101.36a8ccdf84.js.LICENSE.txt */
@@ -1 +0,0 @@
1
- /*! LICENSE: lib-react.dcf2a5e57a.js.LICENSE.txt */
File without changes