@react-native-harness/platform-windows 1.5.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Callstack
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ ![harness-banner](https://react-native-harness.dev/harness-banner.jpg)
2
+
3
+ [![mit licence][license-badge]][license]
4
+ [![npm downloads][npm-downloads-badge]][npm-downloads]
5
+ [![Chat][chat-badge]][chat]
6
+ [![PRs Welcome][prs-welcome-badge]][prs-welcome]
7
+
8
+ React Native Windows platform for React Native Harness — runs your harness tests against a deployed React Native Windows app.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install --save-dev @react-native-harness/platform-windows
14
+ # or
15
+ pnpm add -D @react-native-harness/platform-windows
16
+ # or
17
+ yarn add -D @react-native-harness/platform-windows
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Add the Windows platform to your `rn-harness.config.mjs`:
23
+
24
+ ```javascript
25
+ import { windowsPlatform } from '@react-native-harness/platform-windows';
26
+
27
+ export default {
28
+ entryPoint: './index.js',
29
+ appRegistryComponentName: 'MyApp',
30
+ runners: [
31
+ windowsPlatform({
32
+ name: 'windows',
33
+ // Package.appxmanifest Identity/@Name
34
+ packageName: 'MyApp',
35
+ }),
36
+ ],
37
+ };
38
+ ```
39
+
40
+ Deploy the app before running the harness — the runner launches an already
41
+ installed package, it does not build:
42
+
43
+ ```bash
44
+ npx react-native run-windows --arch x64 --no-launch --no-packager
45
+ npx react-native-harness --harnessRunner windows
46
+ ```
47
+
48
+ ## API
49
+
50
+ ### `windowsPlatform(config)`
51
+
52
+ **Parameters:**
53
+
54
+ - `config.name` — unique name for the runner.
55
+ - `config.packageName` — the app's `Identity/@Name` from `Package.appxmanifest`. Used to look the deployed package up with `Get-AppxPackage`.
56
+ - `config.appId` — the app's `Application/@Id` from `Package.appxmanifest`. Combined with the package family name into the AUMID used to launch the app. Defaults to `App` (the React Native Windows template value).
57
+ - `config.processName` — the app's process name (without `.exe`), for tracking whether it is still running. Defaults to `packageName`.
58
+
59
+ ## Requirements
60
+
61
+ - Windows 10/11 with the app already deployed (`react-native run-windows`).
62
+ - The harness Metro server reachable at the app's configured bundle URL (`http://localhost:8081` by default).
63
+
64
+ ## Made with ❤️ at Callstack
65
+
66
+ `react-native-harness` is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. [Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi!
67
+
68
+ [callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=react-native-harness&utm_term=readme-with-love
69
+ [license-badge]: https://img.shields.io/npm/l/react-native-harness?style=for-the-badge
70
+ [license]: https://github.com/callstackincubator/react-native-harness/blob/main/LICENSE
71
+ [npm-downloads-badge]: https://img.shields.io/npm/dm/react-native-harness?style=for-the-badge
72
+ [npm-downloads]: https://www.npmjs.com/package/react-native-harness
73
+ [prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
74
+ [prs-welcome]: ./CONTRIBUTING.md
75
+ [chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge
76
+ [chat]: https://discord.gg/xgGt7KAjxv
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ export declare const WindowsAppLaunchOptionsSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
3
+ export declare const WindowsPlatformConfigSchema: z.ZodObject<{
4
+ name: z.ZodString;
5
+ /**
6
+ * The app's `Identity/@Name` from `Package.appxmanifest` — e.g.
7
+ * `ReactNativeNitroExample`. Used to look the deployed package up with
8
+ * `Get-AppxPackage`.
9
+ */
10
+ packageName: z.ZodString;
11
+ /**
12
+ * The app's `Application/@Id` from `Package.appxmanifest`. Combined with the
13
+ * package family name into the AUMID used to launch the app. Defaults to
14
+ * `App`, which is what the React Native Windows template generates.
15
+ */
16
+ appId: z.ZodDefault<z.ZodOptional<z.ZodString>>;
17
+ /**
18
+ * The name of the app's process (without `.exe`), for tracking whether it is
19
+ * still running. Defaults to `packageName`, which is correct for the RNW
20
+ * template.
21
+ */
22
+ processName: z.ZodOptional<z.ZodString>;
23
+ appLaunchOptions: z.ZodOptional<z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ name: string;
26
+ packageName: string;
27
+ appId: string;
28
+ processName?: string | undefined;
29
+ appLaunchOptions?: {} | undefined;
30
+ }, {
31
+ name: string;
32
+ packageName: string;
33
+ appId?: string | undefined;
34
+ processName?: string | undefined;
35
+ appLaunchOptions?: {} | undefined;
36
+ }>;
37
+ export type WindowsAppLaunchOptions = z.infer<typeof WindowsAppLaunchOptionsSchema>;
38
+ export type WindowsPlatformConfig = z.infer<typeof WindowsPlatformConfigSchema>;
39
+ /** The `WindowsPlatformConfig` before Zod applies defaults (e.g. `appId`). */
40
+ export type WindowsPlatformConfigInput = z.input<typeof WindowsPlatformConfigSchema>;
41
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,6BAA6B,gDAAe,CAAC;AAE1D,eAAO,MAAM,2BAA2B;;IAEtC;;;;OAIG;;IAEH;;;;OAIG;;IAEH;;;;OAIG;;;;;;;;;;;;;;;EAGH,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF,8EAA8E;AAC9E,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAC9C,OAAO,2BAA2B,CACnC,CAAC"}
package/dist/config.js ADDED
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ export const WindowsAppLaunchOptionsSchema = z.object({});
3
+ export const WindowsPlatformConfigSchema = z.object({
4
+ name: z.string().min(1, 'Name is required'),
5
+ /**
6
+ * The app's `Identity/@Name` from `Package.appxmanifest` — e.g.
7
+ * `ReactNativeNitroExample`. Used to look the deployed package up with
8
+ * `Get-AppxPackage`.
9
+ */
10
+ packageName: z.string().min(1, 'packageName is required'),
11
+ /**
12
+ * The app's `Application/@Id` from `Package.appxmanifest`. Combined with the
13
+ * package family name into the AUMID used to launch the app. Defaults to
14
+ * `App`, which is what the React Native Windows template generates.
15
+ */
16
+ appId: z.string().min(1).optional().default('App'),
17
+ /**
18
+ * The name of the app's process (without `.exe`), for tracking whether it is
19
+ * still running. Defaults to `packageName`, which is correct for the RNW
20
+ * template.
21
+ */
22
+ processName: z.string().min(1).optional(),
23
+ appLaunchOptions: WindowsAppLaunchOptionsSchema.optional(),
24
+ });
@@ -0,0 +1,4 @@
1
+ import { HarnessPlatform } from '@react-native-harness/platforms';
2
+ import type { WindowsPlatformConfigInput } from './config.js';
3
+ export declare const windowsPlatform: (config: WindowsPlatformConfigInput) => HarnessPlatform<WindowsPlatformConfigInput>;
4
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D,eAAO,MAAM,eAAe,GAC1B,QAAQ,0BAA0B,KACjC,eAAe,CAAC,0BAA0B,CAO3C,CAAC"}
@@ -0,0 +1,8 @@
1
+ export const windowsPlatform = (config) => ({
2
+ name: config.name,
3
+ config,
4
+ runner: import.meta.resolve('./runner.js'),
5
+ metroConfigEnhancer: import.meta.resolve('./metro-config-enhancer.js'),
6
+ platformId: 'windows',
7
+ getResourceLockKey: () => `windows:${config.packageName}`,
8
+ });
@@ -0,0 +1,3 @@
1
+ export { windowsPlatform } from './factory.js';
2
+ export type { WindowsPlatformConfig, WindowsPlatformConfigInput, WindowsAppLaunchOptions, } from './config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EACV,qBAAqB,EACrB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { windowsPlatform } from './factory.js';
@@ -0,0 +1,4 @@
1
+ import type { MetroConfigEnhancer } from '@react-native-harness/bundler-metro';
2
+ declare const enhanceMetroConfig: MetroConfigEnhancer;
3
+ export default enhanceMetroConfig;
4
+ //# sourceMappingURL=metro-config-enhancer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metro-config-enhancer.d.ts","sourceRoot":"","sources":["../src/metro-config-enhancer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EAEpB,MAAM,qCAAqC,CAAC;AAqE7C,QAAA,MAAM,kBAAkB,EAAE,mBAmCzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { createRequire } from 'node:module';
2
+ import { logger } from '@react-native-harness/tools';
3
+ const require = createRequire(import.meta.url);
4
+ const log = logger.child('platform-windows');
5
+ const RNW_PACKAGE = 'react-native-windows';
6
+ /**
7
+ * Redirects `react-native` / `react-native/*` imports to `react-native-windows`
8
+ * when Metro is bundling for the `windows` platform, delegating to whatever
9
+ * `resolveRequest` the harness has already composed.
10
+ *
11
+ * `react-native start` installs this via `@react-native/community-cli-plugin`'s
12
+ * `reactNativePlatformResolver`, keyed off the platform's `npmPackageName` in
13
+ * the React Native CLI config. The harness loads Metro's config directly and
14
+ * never runs that plugin, so the platform package supplies the redirect itself.
15
+ */
16
+ const withWindowsPackageRedirect = (next) => {
17
+ const passThrough = next ?? ((ctx, name, plat) => ctx.resolveRequest(ctx, name, plat));
18
+ return (context, moduleName, platform) => {
19
+ if (platform !== 'windows') {
20
+ return passThrough(context, moduleName, platform);
21
+ }
22
+ let redirected = moduleName;
23
+ if (moduleName === 'react-native') {
24
+ redirected = RNW_PACKAGE;
25
+ }
26
+ else if (moduleName.startsWith('react-native/')) {
27
+ redirected = `${RNW_PACKAGE}/${moduleName.slice('react-native/'.length)}`;
28
+ }
29
+ return passThrough(context, redirected, platform);
30
+ };
31
+ };
32
+ /**
33
+ * Resolves `react-native-windows/Libraries/Core/InitializeCore` from the
34
+ * project. Without it running before the entry point a `windows` bundle never
35
+ * calls `setUpBatchedBridge`, so `HMRClient` is not a registered callable
36
+ * module and React Native Windows redboxes the instance before the harness can
37
+ * attach. Metro only emits a `require()` for a run-before module that ends up
38
+ * in the graph, so this is inert for any non-Windows bundle.
39
+ */
40
+ const resolveInitializeCore = (projectRoot) => {
41
+ const specifier = `${RNW_PACKAGE}/Libraries/Core/InitializeCore`;
42
+ try {
43
+ return require.resolve(specifier, { paths: [projectRoot] });
44
+ }
45
+ catch {
46
+ log.warn('could not resolve %s from %s; Windows bundles may fail to initialize', specifier, projectRoot);
47
+ return null;
48
+ }
49
+ };
50
+ const enhanceMetroConfig = (metroConfig, { projectRoot }) => {
51
+ const initializeCore = resolveInitializeCore(projectRoot);
52
+ const existingRunBeforeMainModule = metroConfig.serializer?.getModulesRunBeforeMainModule;
53
+ return {
54
+ ...metroConfig,
55
+ resolver: {
56
+ ...metroConfig.resolver,
57
+ platforms: [
58
+ ...new Set([
59
+ ...(metroConfig.resolver?.platforms ?? ['ios', 'android']),
60
+ 'windows',
61
+ 'native',
62
+ ]),
63
+ ],
64
+ resolveRequest: withWindowsPackageRedirect(metroConfig.resolver?.resolveRequest ?? undefined),
65
+ },
66
+ serializer: {
67
+ ...metroConfig.serializer,
68
+ ...(initializeCore
69
+ ? {
70
+ getModulesRunBeforeMainModule: (entryPoint) => [
71
+ ...(existingRunBeforeMainModule?.(entryPoint) ?? []),
72
+ initializeCore,
73
+ ],
74
+ }
75
+ : {}),
76
+ },
77
+ };
78
+ };
79
+ export default enhanceMetroConfig;
package/dist/pwsh.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Runs a PowerShell snippet non-interactively and returns its trimmed stdout.
3
+ * `-NoProfile` keeps it fast and hermetic; `-NonInteractive` makes sure it
4
+ * never blocks on a prompt.
5
+ */
6
+ export declare const runPowerShell: (script: string) => Promise<string>;
7
+ /**
8
+ * Resolves the PackageFamilyName of a deployed MSIX package from its identity
9
+ * name (`Package.appxmanifest` `Identity/@Name`). Returns `null` when the app
10
+ * is not deployed.
11
+ */
12
+ export declare const getPackageFamilyName: (identityName: string) => Promise<string | null>;
13
+ /** Whether at least one process with the given name (no `.exe`) is running. */
14
+ export declare const isProcessRunning: (processName: string) => Promise<boolean>;
15
+ /**
16
+ * Launches a deployed MSIX app by its AUMID
17
+ * (`<PackageFamilyName>!<Application Id>`).
18
+ *
19
+ * `explorer.exe shell:AppsFolder\<aumid>` is the reliable activation path, but
20
+ * `explorer.exe` almost always exits non-zero even on success, so its failure
21
+ * is swallowed — the caller confirms the app came up by polling for its
22
+ * process.
23
+ */
24
+ export declare const launchAppByAumid: (aumid: string) => Promise<void>;
25
+ /** Force-terminates every process with the given name. Safe to call when none exist. */
26
+ export declare const stopProcess: (processName: string) => Promise<void>;
27
+ //# sourceMappingURL=pwsh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pwsh.d.ts","sourceRoot":"","sources":["../src/pwsh.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,MAAM,CAOlE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,cAAc,MAAM,KACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAKvB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAC3B,aAAa,MAAM,KAClB,OAAO,CAAC,OAAO,CASjB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAU,OAAO,MAAM,KAAG,OAAO,CAAC,IAAI,CAQlE,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,WAAW,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,IAAI,CAQnE,CAAC"}
package/dist/pwsh.js ADDED
@@ -0,0 +1,57 @@
1
+ import { spawn } from '@react-native-harness/tools';
2
+ /**
3
+ * Runs a PowerShell snippet non-interactively and returns its trimmed stdout.
4
+ * `-NoProfile` keeps it fast and hermetic; `-NonInteractive` makes sure it
5
+ * never blocks on a prompt.
6
+ */
7
+ export const runPowerShell = async (script) => {
8
+ const { stdout } = await spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true });
9
+ return stdout.trim();
10
+ };
11
+ /**
12
+ * Resolves the PackageFamilyName of a deployed MSIX package from its identity
13
+ * name (`Package.appxmanifest` `Identity/@Name`). Returns `null` when the app
14
+ * is not deployed.
15
+ */
16
+ export const getPackageFamilyName = async (identityName) => {
17
+ const pfn = await runPowerShell(`(Get-AppxPackage -Name '${identityName}' | Select-Object -First 1).PackageFamilyName`);
18
+ return pfn === '' ? null : pfn;
19
+ };
20
+ /** Whether at least one process with the given name (no `.exe`) is running. */
21
+ export const isProcessRunning = async (processName) => {
22
+ try {
23
+ const count = await runPowerShell(`(Get-Process -Name '${processName}' -ErrorAction SilentlyContinue | Measure-Object).Count`);
24
+ return Number(count) > 0;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ };
30
+ /**
31
+ * Launches a deployed MSIX app by its AUMID
32
+ * (`<PackageFamilyName>!<Application Id>`).
33
+ *
34
+ * `explorer.exe shell:AppsFolder\<aumid>` is the reliable activation path, but
35
+ * `explorer.exe` almost always exits non-zero even on success, so its failure
36
+ * is swallowed — the caller confirms the app came up by polling for its
37
+ * process.
38
+ */
39
+ export const launchAppByAumid = async (aumid) => {
40
+ try {
41
+ await spawn('explorer.exe', [`shell:AppsFolder\\${aumid}`], {
42
+ windowsHide: true,
43
+ });
44
+ }
45
+ catch {
46
+ // Expected: explorer.exe reports a non-zero exit even on success.
47
+ }
48
+ };
49
+ /** Force-terminates every process with the given name. Safe to call when none exist. */
50
+ export const stopProcess = async (processName) => {
51
+ try {
52
+ await runPowerShell(`Get-Process -Name '${processName}' -ErrorAction SilentlyContinue | Stop-Process -Force`);
53
+ }
54
+ catch {
55
+ // Nothing to stop, or it exited between the query and the kill.
56
+ }
57
+ };
@@ -0,0 +1,6 @@
1
+ import { type HarnessPlatformRunnerFactory } from '@react-native-harness/platforms';
2
+ import type { Config as HarnessConfig } from '@react-native-harness/config';
3
+ import { type WindowsPlatformConfigInput } from './config.js';
4
+ declare const getWindowsRunner: HarnessPlatformRunnerFactory<WindowsPlatformConfigInput, HarnessConfig>;
5
+ export default getWindowsRunner;
6
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,4BAA4B,EAClC,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE5E,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,aAAa,CAAC;AA+BrB,QAAA,MAAM,gBAAgB,EAAE,4BAA4B,CAClD,0BAA0B,EAC1B,aAAa,CA6Hd,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
package/dist/runner.js ADDED
@@ -0,0 +1,128 @@
1
+ import { createAppSessionEmitter, AppNotInstalledError, } from '@react-native-harness/platforms';
2
+ import { logger } from '@react-native-harness/tools';
3
+ import { WindowsPlatformConfigSchema, } from './config.js';
4
+ import { getPackageFamilyName, isProcessRunning, launchAppByAumid, stopProcess, } from './pwsh.js';
5
+ const log = logger.child('platform-windows');
6
+ const APP_EXIT_POLL_INTERVAL_MS = 1000;
7
+ const APP_START_POLL_INTERVAL_MS = 400;
8
+ const APP_START_POLL_ATTEMPTS = 15;
9
+ const delay = (ms, signal) => new Promise((resolve, reject) => {
10
+ if (signal.aborted) {
11
+ reject(signal.reason);
12
+ return;
13
+ }
14
+ const timer = setTimeout(() => {
15
+ signal.removeEventListener('abort', onAbort);
16
+ resolve();
17
+ }, ms);
18
+ const onAbort = () => {
19
+ clearTimeout(timer);
20
+ reject(signal.reason);
21
+ };
22
+ signal.addEventListener('abort', onAbort, { once: true });
23
+ });
24
+ const getWindowsRunner = async (config, _harnessConfig, init) => {
25
+ void _harnessConfig;
26
+ const parsedConfig = WindowsPlatformConfigSchema.parse(config);
27
+ const { packageName, appId } = parsedConfig;
28
+ const processName = parsedConfig.processName ?? packageName;
29
+ const packageFamilyName = await getPackageFamilyName(packageName);
30
+ if (packageFamilyName == null) {
31
+ throw new AppNotInstalledError(packageName, 'this machine');
32
+ }
33
+ const aumid = `${packageFamilyName}!${appId}`;
34
+ log.debug('resolved AUMID %s for package %s', aumid, packageName);
35
+ return {
36
+ createAppSession: async () => {
37
+ // Clean slate: never attach to an instance left over from a previous run.
38
+ await stopProcess(processName);
39
+ await launchAppByAumid(aumid);
40
+ // `explorer.exe` returns before the app is up (and lies about its exit
41
+ // code), so confirm the process actually started. `init.signal` cancels
42
+ // this finite readiness wait; it is not a disposal signal.
43
+ let started = false;
44
+ for (let attempt = 0; attempt < APP_START_POLL_ATTEMPTS; attempt += 1) {
45
+ if (await isProcessRunning(processName)) {
46
+ started = true;
47
+ break;
48
+ }
49
+ await delay(APP_START_POLL_INTERVAL_MS, init.signal);
50
+ }
51
+ if (!started) {
52
+ await stopProcess(processName);
53
+ throw new Error(`The Windows app '${processName}' did not start after launching ${aumid}. ` +
54
+ `Deploy it first, e.g. \`npx react-native run-windows --arch x64 --no-launch\`.`);
55
+ }
56
+ const emitter = createAppSessionEmitter();
57
+ let state = { status: 'running' };
58
+ let disposed = false;
59
+ let stopPolling = false;
60
+ let pollDelayTimeout = null;
61
+ let resolvePollDelay = null;
62
+ // Unlike a raced delay() loser (which is fine to just discard), this
63
+ // wait is directly `await`ed by pollTask with nothing else racing it,
64
+ // so cancelling it must also resolve the promise immediately —
65
+ // otherwise dispose() blocks on `await pollTask` for up to
66
+ // APP_EXIT_POLL_INTERVAL_MS instead of returning right away.
67
+ const waitForNextPoll = () => new Promise((resolve) => {
68
+ resolvePollDelay = () => {
69
+ resolvePollDelay = null;
70
+ pollDelayTimeout = null;
71
+ resolve();
72
+ };
73
+ pollDelayTimeout = setTimeout(() => {
74
+ resolvePollDelay?.();
75
+ }, APP_EXIT_POLL_INTERVAL_MS);
76
+ });
77
+ const cancelPendingPollDelay = () => {
78
+ if (pollDelayTimeout) {
79
+ clearTimeout(pollDelayTimeout);
80
+ pollDelayTimeout = null;
81
+ }
82
+ resolvePollDelay?.();
83
+ };
84
+ const pollTask = (async () => {
85
+ while (!stopPolling) {
86
+ if (!(await isProcessRunning(processName))) {
87
+ if (!disposed && state.status === 'running') {
88
+ state = {
89
+ status: 'exited',
90
+ occurredAt: Date.now(),
91
+ reason: 'process-gone',
92
+ };
93
+ emitter.emit({ type: 'app_exited' });
94
+ }
95
+ return;
96
+ }
97
+ if (stopPolling) {
98
+ return;
99
+ }
100
+ await waitForNextPoll();
101
+ }
102
+ })();
103
+ const session = {
104
+ dispose: async () => {
105
+ if (disposed) {
106
+ return;
107
+ }
108
+ disposed = true;
109
+ stopPolling = true;
110
+ cancelPendingPollDelay();
111
+ state = { status: 'disposed', occurredAt: Date.now() };
112
+ emitter.clear();
113
+ await stopProcess(processName);
114
+ await pollTask;
115
+ },
116
+ getState: async () => state,
117
+ getLogs: () => [],
118
+ addListener: emitter.addListener,
119
+ removeListener: emitter.removeListener,
120
+ };
121
+ return session;
122
+ },
123
+ dispose: async () => {
124
+ await stopProcess(processName);
125
+ },
126
+ };
127
+ };
128
+ export default getWindowsRunner;
@@ -0,0 +1,23 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredDependencies: ['vite', 'vitest'],
12
+ ignoredFiles: [
13
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
14
+ '{projectRoot}/src/**/__tests__/**',
15
+ ],
16
+ },
17
+ ],
18
+ },
19
+ languageOptions: {
20
+ parser: await import('jsonc-eslint-parser'),
21
+ },
22
+ },
23
+ ];
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@react-native-harness/platform-windows",
3
+ "description": "React Native Windows platform for React Native Harness",
4
+ "version": "1.5.0",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ "./package.json": "./package.json",
11
+ ".": {
12
+ "development": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "zod": "^3.25.67",
20
+ "tslib": "^2.3.0",
21
+ "@react-native-harness/bundler-metro": "1.5.0",
22
+ "@react-native-harness/config": "1.5.0",
23
+ "@react-native-harness/tools": "1.5.0",
24
+ "@react-native-harness/platforms": "1.5.0"
25
+ },
26
+ "peerDependencies": {
27
+ "metro-config": "*",
28
+ "metro-resolver": "*"
29
+ },
30
+ "devDependencies": {
31
+ "metro-config": "*",
32
+ "metro-resolver": "*"
33
+ },
34
+ "license": "MIT"
35
+ }
package/src/config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+
3
+ export const WindowsAppLaunchOptionsSchema = z.object({});
4
+
5
+ export const WindowsPlatformConfigSchema = z.object({
6
+ name: z.string().min(1, 'Name is required'),
7
+ /**
8
+ * The app's `Identity/@Name` from `Package.appxmanifest` — e.g.
9
+ * `ReactNativeNitroExample`. Used to look the deployed package up with
10
+ * `Get-AppxPackage`.
11
+ */
12
+ packageName: z.string().min(1, 'packageName is required'),
13
+ /**
14
+ * The app's `Application/@Id` from `Package.appxmanifest`. Combined with the
15
+ * package family name into the AUMID used to launch the app. Defaults to
16
+ * `App`, which is what the React Native Windows template generates.
17
+ */
18
+ appId: z.string().min(1).optional().default('App'),
19
+ /**
20
+ * The name of the app's process (without `.exe`), for tracking whether it is
21
+ * still running. Defaults to `packageName`, which is correct for the RNW
22
+ * template.
23
+ */
24
+ processName: z.string().min(1).optional(),
25
+ appLaunchOptions: WindowsAppLaunchOptionsSchema.optional(),
26
+ });
27
+
28
+ export type WindowsAppLaunchOptions = z.infer<
29
+ typeof WindowsAppLaunchOptionsSchema
30
+ >;
31
+ export type WindowsPlatformConfig = z.infer<typeof WindowsPlatformConfigSchema>;
32
+
33
+ /** The `WindowsPlatformConfig` before Zod applies defaults (e.g. `appId`). */
34
+ export type WindowsPlatformConfigInput = z.input<
35
+ typeof WindowsPlatformConfigSchema
36
+ >;
package/src/factory.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { HarnessPlatform } from '@react-native-harness/platforms';
2
+ import type { WindowsPlatformConfigInput } from './config.js';
3
+
4
+ export const windowsPlatform = (
5
+ config: WindowsPlatformConfigInput
6
+ ): HarnessPlatform<WindowsPlatformConfigInput> => ({
7
+ name: config.name,
8
+ config,
9
+ runner: import.meta.resolve('./runner.js'),
10
+ metroConfigEnhancer: import.meta.resolve('./metro-config-enhancer.js'),
11
+ platformId: 'windows',
12
+ getResourceLockKey: () => `windows:${config.packageName}`,
13
+ });
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { windowsPlatform } from './factory.js';
2
+ export type {
3
+ WindowsPlatformConfig,
4
+ WindowsPlatformConfigInput,
5
+ WindowsAppLaunchOptions,
6
+ } from './config.js';
@@ -0,0 +1,112 @@
1
+ import { createRequire } from 'node:module';
2
+ import { logger } from '@react-native-harness/tools';
3
+ import type {
4
+ MetroConfigEnhancer,
5
+ MetroConfigEnhancerContext,
6
+ } from '@react-native-harness/bundler-metro';
7
+ import type { MetroConfig } from 'metro-config';
8
+ import type { CustomResolutionContext, Resolution } from 'metro-resolver';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const log = logger.child('platform-windows');
12
+
13
+ const RNW_PACKAGE = 'react-native-windows';
14
+
15
+ type MetroResolveRequest = (
16
+ context: CustomResolutionContext,
17
+ moduleName: string,
18
+ platform: string | null
19
+ ) => Resolution;
20
+
21
+ /**
22
+ * Redirects `react-native` / `react-native/*` imports to `react-native-windows`
23
+ * when Metro is bundling for the `windows` platform, delegating to whatever
24
+ * `resolveRequest` the harness has already composed.
25
+ *
26
+ * `react-native start` installs this via `@react-native/community-cli-plugin`'s
27
+ * `reactNativePlatformResolver`, keyed off the platform's `npmPackageName` in
28
+ * the React Native CLI config. The harness loads Metro's config directly and
29
+ * never runs that plugin, so the platform package supplies the redirect itself.
30
+ */
31
+ const withWindowsPackageRedirect = (
32
+ next: MetroResolveRequest | undefined
33
+ ): MetroResolveRequest => {
34
+ const passThrough: MetroResolveRequest =
35
+ next ?? ((ctx, name, plat) => ctx.resolveRequest(ctx, name, plat));
36
+
37
+ return (context, moduleName, platform) => {
38
+ if (platform !== 'windows') {
39
+ return passThrough(context, moduleName, platform);
40
+ }
41
+
42
+ let redirected = moduleName;
43
+ if (moduleName === 'react-native') {
44
+ redirected = RNW_PACKAGE;
45
+ } else if (moduleName.startsWith('react-native/')) {
46
+ redirected = `${RNW_PACKAGE}/${moduleName.slice('react-native/'.length)}`;
47
+ }
48
+
49
+ return passThrough(context, redirected, platform);
50
+ };
51
+ };
52
+
53
+ /**
54
+ * Resolves `react-native-windows/Libraries/Core/InitializeCore` from the
55
+ * project. Without it running before the entry point a `windows` bundle never
56
+ * calls `setUpBatchedBridge`, so `HMRClient` is not a registered callable
57
+ * module and React Native Windows redboxes the instance before the harness can
58
+ * attach. Metro only emits a `require()` for a run-before module that ends up
59
+ * in the graph, so this is inert for any non-Windows bundle.
60
+ */
61
+ const resolveInitializeCore = (projectRoot: string): string | null => {
62
+ const specifier = `${RNW_PACKAGE}/Libraries/Core/InitializeCore`;
63
+ try {
64
+ return require.resolve(specifier, { paths: [projectRoot] });
65
+ } catch {
66
+ log.warn(
67
+ 'could not resolve %s from %s; Windows bundles may fail to initialize',
68
+ specifier,
69
+ projectRoot
70
+ );
71
+ return null;
72
+ }
73
+ };
74
+
75
+ const enhanceMetroConfig: MetroConfigEnhancer = (
76
+ metroConfig: MetroConfig,
77
+ { projectRoot }: MetroConfigEnhancerContext
78
+ ): MetroConfig => {
79
+ const initializeCore = resolveInitializeCore(projectRoot);
80
+ const existingRunBeforeMainModule =
81
+ metroConfig.serializer?.getModulesRunBeforeMainModule;
82
+
83
+ return {
84
+ ...metroConfig,
85
+ resolver: {
86
+ ...metroConfig.resolver,
87
+ platforms: [
88
+ ...new Set([
89
+ ...(metroConfig.resolver?.platforms ?? ['ios', 'android']),
90
+ 'windows',
91
+ 'native',
92
+ ]),
93
+ ],
94
+ resolveRequest: withWindowsPackageRedirect(
95
+ metroConfig.resolver?.resolveRequest ?? undefined
96
+ ),
97
+ },
98
+ serializer: {
99
+ ...metroConfig.serializer,
100
+ ...(initializeCore
101
+ ? {
102
+ getModulesRunBeforeMainModule: (entryPoint: string) => [
103
+ ...(existingRunBeforeMainModule?.(entryPoint) ?? []),
104
+ initializeCore,
105
+ ],
106
+ }
107
+ : {}),
108
+ },
109
+ };
110
+ };
111
+
112
+ export default enhanceMetroConfig;
package/src/pwsh.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { spawn } from '@react-native-harness/tools';
2
+
3
+ /**
4
+ * Runs a PowerShell snippet non-interactively and returns its trimmed stdout.
5
+ * `-NoProfile` keeps it fast and hermetic; `-NonInteractive` makes sure it
6
+ * never blocks on a prompt.
7
+ */
8
+ export const runPowerShell = async (script: string): Promise<string> => {
9
+ const { stdout } = await spawn(
10
+ 'powershell',
11
+ ['-NoProfile', '-NonInteractive', '-Command', script],
12
+ { windowsHide: true }
13
+ );
14
+ return stdout.trim();
15
+ };
16
+
17
+ /**
18
+ * Resolves the PackageFamilyName of a deployed MSIX package from its identity
19
+ * name (`Package.appxmanifest` `Identity/@Name`). Returns `null` when the app
20
+ * is not deployed.
21
+ */
22
+ export const getPackageFamilyName = async (
23
+ identityName: string
24
+ ): Promise<string | null> => {
25
+ const pfn = await runPowerShell(
26
+ `(Get-AppxPackage -Name '${identityName}' | Select-Object -First 1).PackageFamilyName`
27
+ );
28
+ return pfn === '' ? null : pfn;
29
+ };
30
+
31
+ /** Whether at least one process with the given name (no `.exe`) is running. */
32
+ export const isProcessRunning = async (
33
+ processName: string
34
+ ): Promise<boolean> => {
35
+ try {
36
+ const count = await runPowerShell(
37
+ `(Get-Process -Name '${processName}' -ErrorAction SilentlyContinue | Measure-Object).Count`
38
+ );
39
+ return Number(count) > 0;
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ /**
46
+ * Launches a deployed MSIX app by its AUMID
47
+ * (`<PackageFamilyName>!<Application Id>`).
48
+ *
49
+ * `explorer.exe shell:AppsFolder\<aumid>` is the reliable activation path, but
50
+ * `explorer.exe` almost always exits non-zero even on success, so its failure
51
+ * is swallowed — the caller confirms the app came up by polling for its
52
+ * process.
53
+ */
54
+ export const launchAppByAumid = async (aumid: string): Promise<void> => {
55
+ try {
56
+ await spawn('explorer.exe', [`shell:AppsFolder\\${aumid}`], {
57
+ windowsHide: true,
58
+ });
59
+ } catch {
60
+ // Expected: explorer.exe reports a non-zero exit even on success.
61
+ }
62
+ };
63
+
64
+ /** Force-terminates every process with the given name. Safe to call when none exist. */
65
+ export const stopProcess = async (processName: string): Promise<void> => {
66
+ try {
67
+ await runPowerShell(
68
+ `Get-Process -Name '${processName}' -ErrorAction SilentlyContinue | Stop-Process -Force`
69
+ );
70
+ } catch {
71
+ // Nothing to stop, or it exited between the query and the kill.
72
+ }
73
+ };
package/src/runner.ts ADDED
@@ -0,0 +1,173 @@
1
+ import {
2
+ createAppSessionEmitter,
3
+ type AppSession,
4
+ type AppSessionState,
5
+ AppNotInstalledError,
6
+ type HarnessPlatformRunnerFactory,
7
+ } from '@react-native-harness/platforms';
8
+ import type { Config as HarnessConfig } from '@react-native-harness/config';
9
+ import { logger } from '@react-native-harness/tools';
10
+ import {
11
+ WindowsPlatformConfigSchema,
12
+ type WindowsPlatformConfigInput,
13
+ } from './config.js';
14
+ import {
15
+ getPackageFamilyName,
16
+ isProcessRunning,
17
+ launchAppByAumid,
18
+ stopProcess,
19
+ } from './pwsh.js';
20
+
21
+ const log = logger.child('platform-windows');
22
+
23
+ const APP_EXIT_POLL_INTERVAL_MS = 1000;
24
+ const APP_START_POLL_INTERVAL_MS = 400;
25
+ const APP_START_POLL_ATTEMPTS = 15;
26
+
27
+ const delay = (ms: number, signal: AbortSignal) =>
28
+ new Promise<void>((resolve, reject) => {
29
+ if (signal.aborted) {
30
+ reject(signal.reason);
31
+ return;
32
+ }
33
+ const timer = setTimeout(() => {
34
+ signal.removeEventListener('abort', onAbort);
35
+ resolve();
36
+ }, ms);
37
+ const onAbort = () => {
38
+ clearTimeout(timer);
39
+ reject(signal.reason);
40
+ };
41
+ signal.addEventListener('abort', onAbort, { once: true });
42
+ });
43
+
44
+ const getWindowsRunner: HarnessPlatformRunnerFactory<
45
+ WindowsPlatformConfigInput,
46
+ HarnessConfig
47
+ > = async (config, _harnessConfig, init) => {
48
+ void _harnessConfig;
49
+
50
+ const parsedConfig = WindowsPlatformConfigSchema.parse(config);
51
+ const { packageName, appId } = parsedConfig;
52
+ const processName = parsedConfig.processName ?? packageName;
53
+
54
+ const packageFamilyName = await getPackageFamilyName(packageName);
55
+
56
+ if (packageFamilyName == null) {
57
+ throw new AppNotInstalledError(packageName, 'this machine');
58
+ }
59
+
60
+ const aumid = `${packageFamilyName}!${appId}`;
61
+ log.debug('resolved AUMID %s for package %s', aumid, packageName);
62
+
63
+ return {
64
+ createAppSession: async (): Promise<AppSession> => {
65
+ // Clean slate: never attach to an instance left over from a previous run.
66
+ await stopProcess(processName);
67
+ await launchAppByAumid(aumid);
68
+
69
+ // `explorer.exe` returns before the app is up (and lies about its exit
70
+ // code), so confirm the process actually started. `init.signal` cancels
71
+ // this finite readiness wait; it is not a disposal signal.
72
+ let started = false;
73
+ for (let attempt = 0; attempt < APP_START_POLL_ATTEMPTS; attempt += 1) {
74
+ if (await isProcessRunning(processName)) {
75
+ started = true;
76
+ break;
77
+ }
78
+ await delay(APP_START_POLL_INTERVAL_MS, init.signal);
79
+ }
80
+
81
+ if (!started) {
82
+ await stopProcess(processName);
83
+ throw new Error(
84
+ `The Windows app '${processName}' did not start after launching ${aumid}. ` +
85
+ `Deploy it first, e.g. \`npx react-native run-windows --arch x64 --no-launch\`.`
86
+ );
87
+ }
88
+
89
+ const emitter = createAppSessionEmitter();
90
+ let state: AppSessionState = { status: 'running' };
91
+ let disposed = false;
92
+ let stopPolling = false;
93
+ let pollDelayTimeout: ReturnType<typeof setTimeout> | null = null;
94
+ let resolvePollDelay: (() => void) | null = null;
95
+
96
+ // Unlike a raced delay() loser (which is fine to just discard), this
97
+ // wait is directly `await`ed by pollTask with nothing else racing it,
98
+ // so cancelling it must also resolve the promise immediately —
99
+ // otherwise dispose() blocks on `await pollTask` for up to
100
+ // APP_EXIT_POLL_INTERVAL_MS instead of returning right away.
101
+ const waitForNextPoll = () =>
102
+ new Promise<void>((resolve) => {
103
+ resolvePollDelay = () => {
104
+ resolvePollDelay = null;
105
+ pollDelayTimeout = null;
106
+ resolve();
107
+ };
108
+
109
+ pollDelayTimeout = setTimeout(() => {
110
+ resolvePollDelay?.();
111
+ }, APP_EXIT_POLL_INTERVAL_MS);
112
+ });
113
+
114
+ const cancelPendingPollDelay = () => {
115
+ if (pollDelayTimeout) {
116
+ clearTimeout(pollDelayTimeout);
117
+ pollDelayTimeout = null;
118
+ }
119
+
120
+ resolvePollDelay?.();
121
+ };
122
+
123
+ const pollTask = (async () => {
124
+ while (!stopPolling) {
125
+ if (!(await isProcessRunning(processName))) {
126
+ if (!disposed && state.status === 'running') {
127
+ state = {
128
+ status: 'exited',
129
+ occurredAt: Date.now(),
130
+ reason: 'process-gone',
131
+ };
132
+ emitter.emit({ type: 'app_exited' });
133
+ }
134
+ return;
135
+ }
136
+
137
+ if (stopPolling) {
138
+ return;
139
+ }
140
+
141
+ await waitForNextPoll();
142
+ }
143
+ })();
144
+
145
+ const session: AppSession = {
146
+ dispose: async () => {
147
+ if (disposed) {
148
+ return;
149
+ }
150
+
151
+ disposed = true;
152
+ stopPolling = true;
153
+ cancelPendingPollDelay();
154
+ state = { status: 'disposed', occurredAt: Date.now() };
155
+ emitter.clear();
156
+ await stopProcess(processName);
157
+ await pollTask;
158
+ },
159
+ getState: async () => state,
160
+ getLogs: () => [],
161
+ addListener: emitter.addListener,
162
+ removeListener: emitter.removeListener,
163
+ };
164
+
165
+ return session;
166
+ },
167
+ dispose: async () => {
168
+ await stopProcess(processName);
169
+ },
170
+ };
171
+ };
172
+
173
+ export default getWindowsRunner;
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "../tools"
8
+ },
9
+ {
10
+ "path": "../platforms"
11
+ },
12
+ {
13
+ "path": "../config"
14
+ },
15
+ {
16
+ "path": "../bundler-metro"
17
+ },
18
+ {
19
+ "path": "./tsconfig.lib.json"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
8
+ "emitDeclarationOnly": false,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src/**/*.ts"],
13
+ "references": [
14
+ {
15
+ "path": "../tools/tsconfig.lib.json"
16
+ },
17
+ {
18
+ "path": "../platforms/tsconfig.lib.json"
19
+ },
20
+ {
21
+ "path": "../config/tsconfig.lib.json"
22
+ },
23
+ {
24
+ "path": "../bundler-metro/tsconfig.lib.json"
25
+ }
26
+ ]
27
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,18 @@
1
+ /// <reference types='vitest' />
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig(() => ({
5
+ root: __dirname,
6
+ cacheDir: '../../node_modules/.vite/packages/platform-windows',
7
+ test: {
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ reporters: ['default'],
13
+ coverage: {
14
+ reportsDirectory: './test-output/vitest/coverage',
15
+ provider: 'v8' as const,
16
+ },
17
+ },
18
+ }));