From 54e9dcc638b94ff08a0995c9599302c3251ca7c8 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 17:15:44 +0100 Subject: [PATCH 1/5] feat: add config wizard --- actions/shared/index.cjs | 218 +++++++++++---------- apps/playground/jest.config.js | 17 -- apps/playground/jest.harness.config.mjs | 4 + apps/playground/rn-harness.config.mjs | 59 +----- packages/cli/eslint.config.mjs | 2 +- packages/cli/package.json | 6 +- packages/cli/src/index.ts | 37 +++- packages/cli/src/wizard/bundleId.ts | 53 +++++ packages/cli/src/wizard/configGenerator.ts | 112 +++++++++++ packages/cli/src/wizard/index.ts | 72 +++++++ packages/cli/src/wizard/jestConfig.ts | 28 +++ packages/cli/src/wizard/platforms.ts | 41 ++++ packages/cli/src/wizard/projectType.ts | 77 ++++++++ packages/cli/src/wizard/targets.ts | 64 ++++++ packages/cli/tsconfig.json | 12 ++ packages/cli/tsconfig.lib.json | 12 ++ packages/platform-android/src/adb.ts | 50 +++++ packages/platform-android/src/index.ts | 1 + packages/platform-android/src/targets.ts | 38 ++++ packages/platform-ios/src/index.ts | 1 + packages/platform-ios/src/targets.ts | 49 +++++ packages/platforms/src/index.ts | 6 +- packages/platforms/src/types.ts | 8 + packages/react-native-harness/package.json | 3 +- packages/tools/package.json | 2 +- packages/tools/src/index.ts | 1 + packages/tools/src/packages.ts | 73 +++++++ packages/tools/src/prompts.ts | 36 +++- pnpm-lock.yaml | 54 +++-- 29 files changed, 935 insertions(+), 201 deletions(-) delete mode 100644 apps/playground/jest.config.js create mode 100644 apps/playground/jest.harness.config.mjs create mode 100644 packages/cli/src/wizard/bundleId.ts create mode 100644 packages/cli/src/wizard/configGenerator.ts create mode 100644 packages/cli/src/wizard/index.ts create mode 100644 packages/cli/src/wizard/jestConfig.ts create mode 100644 packages/cli/src/wizard/platforms.ts create mode 100644 packages/cli/src/wizard/projectType.ts create mode 100644 packages/cli/src/wizard/targets.ts create mode 100644 packages/platform-android/src/targets.ts create mode 100644 packages/platform-ios/src/targets.ts create mode 100644 packages/tools/src/packages.ts diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index 41e40ce..f858404 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -110,14 +110,14 @@ var require_src = __commonJS({ var CSI = `${ESC}[`; var beep = "\x07"; var cursor = { - to(x, y) { - if (!y) return `${CSI}${x + 1}G`; - return `${CSI}${y + 1};${x + 1}H`; + to(x2, y) { + if (!y) return `${CSI}${x2 + 1}G`; + return `${CSI}${y + 1};${x2 + 1}H`; }, - move(x, y) { + move(x2, y) { let ret = ""; - if (x < 0) ret += `${CSI}${-x}D`; - else if (x > 0) ret += `${CSI}${x}C`; + if (x2 < 0) ret += `${CSI}${-x2}D`; + else if (x2 > 0) ret += `${CSI}${x2}C`; if (y < 0) ret += `${CSI}${-y}A`; else if (y > 0) ret += `${CSI}${y}B`; return ret; @@ -287,7 +287,7 @@ __export(external_exports, { // ../../node_modules/zod/dist/esm/v3/helpers/util.js var util; (function(util3) { - util3.assertEqual = (_) => { + util3.assertEqual = (_2) => { }; function assertIs(_arg) { } @@ -304,10 +304,10 @@ var util; return obj; }; util3.getValidEnumValues = (obj) => { - const validKeys = util3.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number"); + const validKeys = util3.objectKeys(obj).filter((k3) => typeof obj[obj[k3]] !== "number"); const filtered = {}; - for (const k of validKeys) { - filtered[k] = obj[k]; + for (const k3 of validKeys) { + filtered[k3] = obj[k3]; } return util3.objectValues(filtered); }; @@ -337,7 +337,7 @@ var util; return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator); } util3.joinValues = joinValues; - util3.jsonStringifyReplacer = (_, value) => { + util3.jsonStringifyReplacer = (_2, value) => { if (typeof value === "bigint") { return value.toString(); } @@ -647,8 +647,8 @@ function getErrorMap() { // ../../node_modules/zod/dist/esm/v3/helpers/parseUtil.js var makeIssue = (params) => { - const { data, path: path4, errorMaps, issueData } = params; - const fullPath = [...path4, ...issueData.path || []]; + const { data, path: path5, errorMaps, issueData } = params; + const fullPath = [...path5, ...issueData.path || []]; const fullIssue = { ...issueData, path: fullPath @@ -687,7 +687,7 @@ function addIssueToContext(ctx, issueData) { // then global override map overrideMap === en_default ? void 0 : en_default // then global default map - ].filter((x) => !!x) + ].filter((x2) => !!x2) }); ctx.common.issues.push(issue); } @@ -750,10 +750,10 @@ var INVALID = Object.freeze({ }); var DIRTY = (value) => ({ status: "dirty", value }); var OK = (value) => ({ status: "valid", value }); -var isAborted = (x) => x.status === "aborted"; -var isDirty = (x) => x.status === "dirty"; -var isValid = (x) => x.status === "valid"; -var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise; +var isAborted = (x2) => x2.status === "aborted"; +var isDirty = (x2) => x2.status === "dirty"; +var isValid = (x2) => x2.status === "valid"; +var isAsync = (x2) => typeof Promise !== "undefined" && x2 instanceof Promise; // ../../node_modules/zod/dist/esm/v3/helpers/errorUtil.js var errorUtil; @@ -764,11 +764,11 @@ var errorUtil; // ../../node_modules/zod/dist/esm/v3/types.js var ParseInputLazyPath = class { - constructor(parent, value, path4, key) { + constructor(parent, value, path5, key) { this._cachedPath = []; this.parent = parent; this.data = value; - this._path = path4; + this._path = path5; this._key = key; } get path() { @@ -3071,17 +3071,17 @@ var ZodDiscriminatedUnion = class _ZodDiscriminatedUnion extends ZodType { }); } }; -function mergeValues(a, b2) { +function mergeValues(a, b) { const aType = getParsedType(a); - const bType = getParsedType(b2); - if (a === b2) { + const bType = getParsedType(b); + if (a === b) { return { valid: true, data: a }; } else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { - const bKeys = util.objectKeys(b2); + const bKeys = util.objectKeys(b); const sharedKeys = util.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); - const newObj = { ...a, ...b2 }; + const newObj = { ...a, ...b }; for (const key of sharedKeys) { - const sharedValue = mergeValues(a[key], b2[key]); + const sharedValue = mergeValues(a[key], b[key]); if (!sharedValue.valid) { return { valid: false }; } @@ -3089,13 +3089,13 @@ function mergeValues(a, b2) { } return { valid: true, data: newObj }; } else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { - if (a.length !== b2.length) { + if (a.length !== b.length) { return { valid: false }; } const newArray = []; for (let index = 0; index < a.length; index++) { const itemA = a[index]; - const itemB = b2[index]; + const itemB = b[index]; const sharedValue = mergeValues(itemA, itemB); if (!sharedValue.valid) { return { valid: false }; @@ -3103,7 +3103,7 @@ function mergeValues(a, b2) { newArray.push(sharedValue.data); } return { valid: true, data: newArray }; - } else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b2) { + } else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { return { valid: true, data: a }; } else { return { valid: false }; @@ -3199,7 +3199,7 @@ var ZodTuple = class _ZodTuple extends ZodType { if (!schema) return null; return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); - }).filter((x) => !!x); + }).filter((x2) => !!x2); if (ctx.common.async) { return Promise.all(items).then((results) => { return ParseStatus.mergeArray(status, results); @@ -3452,7 +3452,7 @@ var ZodFunction = class _ZodFunction extends ZodType { return makeIssue({ data: args, path: ctx.path, - errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x) => !!x), + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), issueData: { code: ZodIssueCode.invalid_arguments, argumentsError: error @@ -3463,7 +3463,7 @@ var ZodFunction = class _ZodFunction extends ZodType { return makeIssue({ data: returns, path: ctx.path, - errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x) => !!x), + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), issueData: { code: ZodIssueCode.invalid_return_type, returnTypeError: error @@ -3473,29 +3473,29 @@ var ZodFunction = class _ZodFunction extends ZodType { const params = { errorMap: ctx.common.contextualErrorMap }; const fn = ctx.data; if (this._def.returns instanceof ZodPromise) { - const me = this; + const me2 = this; return OK(async function(...args) { const error = new ZodError([]); - const parsedArgs = await me._def.args.parseAsync(args, params).catch((e) => { + const parsedArgs = await me2._def.args.parseAsync(args, params).catch((e) => { error.addIssue(makeArgsIssue(args, e)); throw error; }); const result = await Reflect.apply(fn, this, parsedArgs); - const parsedReturns = await me._def.returns._def.type.parseAsync(result, params).catch((e) => { + const parsedReturns = await me2._def.returns._def.type.parseAsync(result, params).catch((e) => { error.addIssue(makeReturnsIssue(result, e)); throw error; }); return parsedReturns; }); } else { - const me = this; + const me2 = this; return OK(function(...args) { - const parsedArgs = me._def.args.safeParse(args, params); + const parsedArgs = me2._def.args.safeParse(args, params); if (!parsedArgs.success) { throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); } const result = Reflect.apply(fn, this, parsedArgs.data); - const parsedReturns = me._def.returns.safeParse(result, params); + const parsedReturns = me2._def.returns.safeParse(result, params); if (!parsedReturns.success) { throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); } @@ -4057,10 +4057,10 @@ var ZodPipeline = class _ZodPipeline extends ZodType { } } } - static create(a, b2) { + static create(a, b) { return new _ZodPipeline({ in: a, - out: b2, + out: b, typeName: ZodFirstPartyTypeKind.ZodPipeline }); } @@ -4209,16 +4209,25 @@ var coerce = { var NEVER = INVALID; // ../config/dist/types.js +var RunnerSchema = external_exports.object({ + name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), + config: external_exports.record(external_exports.any()), + runner: external_exports.string() +}); var ConfigSchema = external_exports.object({ entryPoint: external_exports.string().min(1, "Entry point is required"), appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"), - runners: external_exports.array(external_exports.any()).min(1, "At least one runner is required"), + runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"), defaultRunner: external_exports.string().optional(), webSocketPort: external_exports.number().optional().default(3001), bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4), + bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(15e3), + maxAppRestarts: external_exports.number().min(0, "Max app restarts must be non-negative").default(2), resetEnvironmentBetweenTestFiles: external_exports.boolean().optional().default(true), unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().default(false), unstable__enableMetroCache: external_exports.boolean().optional().default(false), + detectNativeCrashes: external_exports.boolean().optional().default(true), + crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), // Deprecated property - used for migration detection include: external_exports.array(external_exports.string()).optional() }).refine((config) => { @@ -4236,21 +4245,21 @@ var import_node_util2 = __toESM(require("util"), 1); // ../../node_modules/@clack/core/dist/index.mjs var import_node_process = require("process"); -var V = __toESM(require("readline"), 1); +var k = __toESM(require("readline"), 1); var import_node_readline = __toESM(require("readline"), 1); var import_sisteransi = __toESM(require_src(), 1); var import_node_tty = require("tty"); -var lt = { limit: 1 / 0, ellipsis: "" }; -var ht = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; -var P = "\x07"; -var X = "["; -var Ft = "]"; -var O = `${Ft}8;;`; -var Q = new RegExp(`(?:\\${X}(?\\d+)m|\\${O}(?.*)${P})`, "y"); -var gt = ["up", "down", "left", "right", "space", "enter", "cancel"]; -var C = { actions: new Set(gt), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" } }; -var At = globalThis.process.platform.startsWith("win"); -var G = Symbol("clack:cancel"); +var Ft = { limit: 1 / 0, ellipsis: "" }; +var ft = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var j = "\x07"; +var Q = "["; +var dt = "]"; +var U = `${dt}8;;`; +var et = new RegExp(`(?:\\${Q}(?\\d+)m|\\${U}(?.*)${j})`, "y"); +var At = ["up", "down", "left", "right", "space", "enter", "cancel"]; +var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true }; +var bt = globalThis.process.platform.startsWith("win"); +var z = Symbol("clack:cancel"); // ../../node_modules/@clack/prompts/dist/index.mjs var import_picocolors = __toESM(require_picocolors(), 1); @@ -4259,50 +4268,51 @@ var import_node_fs = require("fs"); var import_node_path = require("path"); var import_sisteransi2 = __toESM(require_src(), 1); var import_node_util = require("util"); -function ot() { +function ht() { return import_node_process2.default.platform !== "win32" ? import_node_process2.default.env.TERM !== "linux" : !!import_node_process2.default.env.CI || !!import_node_process2.default.env.WT_SESSION || !!import_node_process2.default.env.TERMINUS_SUBLIME || import_node_process2.default.env.ConEmuTask === "{cmd::Cmder}" || import_node_process2.default.env.TERM_PROGRAM === "Terminus-Sublime" || import_node_process2.default.env.TERM_PROGRAM === "vscode" || import_node_process2.default.env.TERM === "xterm-256color" || import_node_process2.default.env.TERM === "alacritty" || import_node_process2.default.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; } -var J = ot(); -var S = (e, r) => J ? e : r; -var we = S("\u25C6", "*"); -var ie = S("\u25A0", "x"); -var ne = S("\u25B2", "x"); -var W = S("\u25C7", "o"); -var ae = S("\u250C", "T"); -var c = S("\u2502", "|"); -var b = S("\u2514", "\u2014"); -var Be = S("\u2510", "T"); -var xe = S("\u2518", "\u2014"); -var U = S("\u25CF", ">"); -var K = S("\u25CB", " "); -var X2 = S("\u25FB", "[\u2022]"); -var P2 = S("\u25FC", "[+]"); -var Y2 = S("\u25FB", "[ ]"); -var be = S("\u25AA", "\u2022"); -var z2 = S("\u2500", "-"); -var oe = S("\u256E", "+"); -var _e = S("\u251C", "+"); -var le = S("\u256F", "+"); -var De = S("\u2570", "+"); -var Te = S("\u256D", "+"); -var ue = S("\u25CF", "\u2022"); -var ce = S("\u25C6", "*"); -var $e = S("\u25B2", "!"); -var de = S("\u25A0", "x"); -var dt = { limit: 1 / 0, ellipsis: "" }; -var ht2 = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; -var pe = "\x07"; -var Oe = "["; -var pt = "]"; -var ge = `${pt}8;;`; -var Le = new RegExp(`(?:\\${Oe}(?\\d+)m|\\${ge}(?.*)${pe})`, "y"); -var He = { light: S("\u2500", "-"), heavy: S("\u2501", "="), block: S("\u2588", "#") }; -var Ue = `${import_picocolors.default.gray(c)} `; +var ee = ht(); +var w = (e, r) => ee ? e : r; +var Me = w("\u25C6", "*"); +var ce = w("\u25A0", "x"); +var de = w("\u25B2", "x"); +var V = w("\u25C7", "o"); +var $e = w("\u250C", "T"); +var h = w("\u2502", "|"); +var x = w("\u2514", "\u2014"); +var Re = w("\u2510", "T"); +var Oe = w("\u2518", "\u2014"); +var Y = w("\u25CF", ">"); +var K = w("\u25CB", " "); +var te = w("\u25FB", "[\u2022]"); +var k2 = w("\u25FC", "[+]"); +var z2 = w("\u25FB", "[ ]"); +var Pe = w("\u25AA", "\u2022"); +var se = w("\u2500", "-"); +var he = w("\u256E", "+"); +var Ne = w("\u251C", "+"); +var me = w("\u256F", "+"); +var pe = w("\u2570", "+"); +var We = w("\u256D", "+"); +var ge = w("\u25CF", "\u2022"); +var fe = w("\u25C6", "*"); +var Fe = w("\u25B2", "!"); +var ye = w("\u25A0", "x"); +var Ft2 = { limit: 1 / 0, ellipsis: "" }; +var yt2 = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var Ce = "\x07"; +var Ve = "["; +var vt = "]"; +var we = `${vt}8;;`; +var Ge = new RegExp(`(?:\\${Ve}(?\\d+)m|\\${we}(?.*)${Ce})`, "y"); +var Ut = import_picocolors.default.magenta; +var Ye = { light: w("\u2500", "-"), heavy: w("\u2501", "="), block: w("\u2588", "#") }; +var ze = `${import_picocolors.default.gray(h)} `; // ../tools/dist/logger.js var import_is_unicode_supported = __toESM(require_is_unicode_supported(), 1); var unicode = (0, import_is_unicode_supported.default)(); -var unicodeWithFallback = (c2, fallback) => unicode ? c2 : fallback; +var unicodeWithFallback = (c, fallback) => unicode ? c : fallback; var SYMBOL_DEBUG = unicodeWithFallback("\u25CF", "\u2022"); var verbose = !!process.env.HARNESS_DEBUG; @@ -4315,6 +4325,10 @@ var import_node_fs2 = __toESM(require("fs"), 1); var HarnessError = class extends Error { }; +// ../tools/dist/packages.js +var import_node_path3 = __toESM(require("path"), 1); +var import_node_fs3 = __toESM(require("fs"), 1); + // ../config/dist/errors.js var ConfigValidationError = class extends HarnessError { filePath; @@ -4382,16 +4396,16 @@ var ConfigLoadError = class extends HarnessError { }; // ../config/dist/reader.js -var import_node_path3 = __toESM(require("path"), 1); -var import_node_fs3 = __toESM(require("fs"), 1); +var import_node_path4 = __toESM(require("path"), 1); +var import_node_fs4 = __toESM(require("fs"), 1); var import_node_module2 = require("module"); var import_meta = {}; var extensions = [".js", ".mjs", ".cjs", ".json"]; var importUp = async (dir, name) => { - const filePath = import_node_path3.default.join(dir, name); + const filePath = import_node_path4.default.join(dir, name); for (const ext of extensions) { const filePathWithExt = `${filePath}${ext}`; - if (import_node_fs3.default.existsSync(filePathWithExt)) { + if (import_node_fs4.default.existsSync(filePathWithExt)) { let rawConfig; try { if (ext === ".mjs") { @@ -4409,8 +4423,8 @@ var importUp = async (dir, name) => { } catch (error) { if (error instanceof ZodError) { const validationErrors = error.errors.map((err) => { - const path4 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : ""; - return `${err.message}${path4}`; + const path5 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : ""; + return `${err.message}${path5}`; }); throw new ConfigValidationError(filePathWithExt, validationErrors); } @@ -4418,7 +4432,7 @@ var importUp = async (dir, name) => { } } } - const parentDir = import_node_path3.default.dirname(dir); + const parentDir = import_node_path4.default.dirname(dir); if (parentDir === dir) { throw new ConfigNotFoundError(dir); } @@ -4433,8 +4447,8 @@ var getConfig = async (dir) => { }; // src/shared/index.ts -var import_node_path4 = __toESM(require("path")); -var import_node_fs4 = __toESM(require("fs")); +var import_node_path5 = __toESM(require("path")); +var import_node_fs5 = __toESM(require("fs")); var run = async () => { try { const projectRootInput = process.env.INPUT_PROJECTROOT; @@ -4442,7 +4456,7 @@ var run = async () => { if (!runnerInput) { throw new Error("Runner input is required"); } - const projectRoot = projectRootInput ? import_node_path4.default.resolve(projectRootInput) : process.cwd(); + const projectRoot = projectRootInput ? import_node_path5.default.resolve(projectRootInput) : process.cwd(); console.info(`Loading React Native Harness config from: ${projectRoot}`); const { config } = await getConfig(projectRoot); const runner = config.runners.find((runner2) => runner2.name === runnerInput); @@ -4455,7 +4469,7 @@ var run = async () => { } const output = `config=${JSON.stringify(runner)} `; - import_node_fs4.default.appendFileSync(githubOutput, output); + import_node_fs5.default.appendFileSync(githubOutput, output); } catch (error) { if (error instanceof Error) { console.error(error.message); diff --git a/apps/playground/jest.config.js b/apps/playground/jest.config.js deleted file mode 100644 index d785bdd..0000000 --- a/apps/playground/jest.config.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - projects: [ - { - displayName: 'react-native-harness', - preset: 'react-native-harness', - testMatch: [ - '/src/__tests__/**/*.(test|spec|harness).(js|jsx|ts|tsx)', - ], - setupFiles: ['./src/setupFile.ts'], - setupFilesAfterEnv: ['./src/setupFileAfterEnv.ts'], - // This is necessary to prevent Jest from transforming the workspace packages. - // Not needed in users projects, as they will have the packages installed in their node_modules. - transformIgnorePatterns: ['/packages/', '/node_modules/'], - }, - ], - collectCoverageFrom: ['./src/**/*.(ts|tsx)'], -}; diff --git a/apps/playground/jest.harness.config.mjs b/apps/playground/jest.harness.config.mjs new file mode 100644 index 0000000..c9e6f90 --- /dev/null +++ b/apps/playground/jest.harness.config.mjs @@ -0,0 +1,4 @@ +export default { + preset: 'react-native-harness', + testMatch: ['/**/__tests__/**/*.harness.[jt]s?(x)'], +}; diff --git a/apps/playground/rn-harness.config.mjs b/apps/playground/rn-harness.config.mjs index 0c49b64..740fb3f 100644 --- a/apps/playground/rn-harness.config.mjs +++ b/apps/playground/rn-harness.config.mjs @@ -1,60 +1,21 @@ -import { - androidPlatform, - androidEmulator, - physicalAndroidDevice, -} from '@react-native-harness/platform-android'; -import { - applePlatform, - applePhysicalDevice, - appleSimulator, -} from '@react-native-harness/platform-apple'; -import { - vegaPlatform, - vegaEmulator, -} from '@react-native-harness/platform-vega'; +import { androidPlatform, androidEmulator } from "@react-native-harness/platform-android"; +import { applePlatform, appleSimulator } from "@react-native-harness/platform-apple"; -const config = { +export default { entryPoint: './index.js', appRegistryComponentName: 'HarnessPlayground', runners: [ androidPlatform({ - name: 'android', - device: androidEmulator('Pixel_8_API_35', { - apiLevel: 35, - profile: 'pixel_6', - diskSize: '1G', - heapSize: '1G', - }), - bundleId: 'com.harnessplayground', - }), - androidPlatform({ - name: 'moto-g72', - device: physicalAndroidDevice('Motorola', 'Moto G72'), - bundleId: 'com.harnessplayground', - }), - applePlatform({ - name: 'iphone-16-pro', - device: applePhysicalDevice('iPhone (Szymon) (2)'), - bundleId: 'react-native-harness', + name: 'pixel_8_api_33', + device: androidEmulator('Pixel_8_API_33'), + bundleId: 'com.example', }), applePlatform({ - name: 'ios', - device: appleSimulator('iPhone 16 Pro', '18.6'), - bundleId: 'com.harnessplayground', - }), - vegaPlatform({ - name: 'vega', - device: vegaEmulator('VegaTV_1'), - bundleId: 'com.playground', + name: 'iphone-16-pro-max', + device: appleSimulator('iPhone 16 Pro Max', '26.0'), + bundleId: 'com.example', }), ], - defaultRunner: 'android', - bridgeTimeout: 120000, - webSocketPort: 3002, - - resetEnvironmentBetweenTestFiles: true, - unstable__skipAlreadyIncludedModules: false, + defaultRunner: 'pixel_8_api_33', }; - -export default config; diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs index 1b6d677..54c1935 100644 --- a/packages/cli/eslint.config.mjs +++ b/packages/cli/eslint.config.mjs @@ -9,7 +9,7 @@ export default [ 'error', { ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], - ignoredDependencies: ['@react-native-harness/bridge'], + ignoredDependencies: ['@react-native-harness/bridge', '@react-native-harness/platform-android', '@react-native-harness/platform-apple'], }, ], }, diff --git a/packages/cli/package.json b/packages/cli/package.json index fadba61..3b6440d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,10 +23,14 @@ "dependencies": { "@react-native-harness/bridge": "workspace:*", "@react-native-harness/config": "workspace:*", + "@react-native-harness/platforms": "workspace:*", + "@react-native-harness/tools": "workspace:*", "tslib": "^2.3.0" }, "devDependencies": { - "jest-cli": "^30.2.0" + "jest-cli": "^30.2.0", + "@react-native-harness/platform-android": "workspace:*", + "@react-native-harness/platform-apple": "workspace:*" }, "peerDependencies": { "jest-cli": "*" diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f467583..9522515 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,12 +1,18 @@ import { run, yargsOptions } from 'jest-cli'; import { getConfig } from '@react-native-harness/config'; +import { runInitWizard } from './wizard/index.js'; +import fs from 'node:fs'; +import path from 'node:path'; + +const JEST_CONFIG_EXTENSIONS = ['.mjs', '.js', '.cjs']; +const JEST_HARNESS_CONFIG_BASE = 'jest.harness.config'; const checkForOldConfig = async () => { try { const { config } = await getConfig(process.cwd()); if (config.include) { - console.error('\n❌ Migration Required\n'); + console.error('\n❌ Migration required\n'); console.error('React Native Harness has migrated to the Jest CLI.'); console.error( 'The "include" property in your rn-harness.config file is no longer supported.\n' @@ -27,7 +33,7 @@ const checkForOldConfig = async () => { const patchYargsOptions = () => { yargsOptions.harnessRunner = { type: 'string', - description: 'Specify which Harness runner to use', + description: 'Specify which harness runner to use', requiresArg: true, }; @@ -67,5 +73,28 @@ const patchYargsOptions = () => { delete yargsOptions.logHeapUsage; }; -patchYargsOptions(); -checkForOldConfig().then(() => run()); +if (process.argv.includes('init')) { + runInitWizard(); +} else { + patchYargsOptions(); + + const hasConfigArg = + process.argv.includes('--config') || process.argv.includes('-c'); + + if (!hasConfigArg) { + const existingConfigExt = JEST_CONFIG_EXTENSIONS.find((ext) => + fs.existsSync( + path.join(process.cwd(), `${JEST_HARNESS_CONFIG_BASE}${ext}`) + ) + ); + + if (existingConfigExt) { + process.argv.push( + '--config', + `${JEST_HARNESS_CONFIG_BASE}${existingConfigExt}` + ); + } + } + + checkForOldConfig().then(() => run()); +} diff --git a/packages/cli/src/wizard/bundleId.ts b/packages/cli/src/wizard/bundleId.ts new file mode 100644 index 0000000..549714b --- /dev/null +++ b/packages/cli/src/wizard/bundleId.ts @@ -0,0 +1,53 @@ +import { promptText } from '@react-native-harness/tools'; + +export const getBundleIds = async ( + selectedPlatforms: string[] +): Promise> => { + const bundleIds: Record = {}; + + if (selectedPlatforms.includes('android')) { + bundleIds.android = await promptText({ + message: 'Enter Android package name', + placeholder: 'com.example.app', + validate: (value: string | undefined) => { + if (!value) return 'Package name is required'; + const parts = value.split('.'); + if (parts.length < 2) { + return 'Package name must have at least two segments (e.g., com.example)'; + } + for (const segment of parts) { + if (!segment) return 'Segments cannot be empty'; + if (!/^[a-zA-Z]/.test(segment)) { + return `Segment "${segment}" must start with a letter`; + } + if (!/^[a-zA-Z0-9_]+$/.test(segment)) { + return `Segment "${segment}" can only contain alphanumeric characters or underscores`; + } + } + return; + }, + }); + } + + if (selectedPlatforms.includes('ios')) { + bundleIds.ios = await promptText({ + message: 'Enter iOS bundle identifier', + placeholder: 'com.example.app', + validate: (value: string | undefined) => { + if (!value) return 'Bundle identifier is required'; + if (!/^[a-zA-Z0-9.-]+$/.test(value)) { + return 'Bundle identifier can only contain alphanumeric characters, hyphens, and periods'; + } + if (value.startsWith('.') || value.endsWith('.')) { + return 'Bundle identifier cannot start or end with a period'; + } + if (value.includes('..')) { + return 'Bundle identifier cannot contain consecutive periods'; + } + return; + }, + }); + } + + return bundleIds; +}; diff --git a/packages/cli/src/wizard/configGenerator.ts b/packages/cli/src/wizard/configGenerator.ts new file mode 100644 index 0000000..cdbff84 --- /dev/null +++ b/packages/cli/src/wizard/configGenerator.ts @@ -0,0 +1,112 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { RunTarget } from '@react-native-harness/platforms'; +import type { ProjectConfig } from './projectType.js'; + +const q = (s: string) => `'${s.replace(/'/g, "\\'")}'`; + +const getDeviceCall = (target: RunTarget): string => { + const { device, type, platform } = target; + if (platform === 'android') { + if (type === 'emulator') { + return `androidEmulator(${q(device.name)})`; + } + return `physicalAndroidDevice(${q(device.manufacturer)}, ${q( + device.model + )})`; + } + if (platform === 'ios') { + if (type === 'emulator') { + return `appleSimulator(${q(device.name)}, ${q(device.systemVersion)})`; + } + return `applePhysicalDevice(${q(device.name)})`; + } + return JSON.stringify(device); +}; + +const getPlatformFn = (platform: string): string => { + if (platform === 'android') return 'androidPlatform'; + if (platform === 'ios') return 'applePlatform'; + return `${platform}Platform`; +}; + +export const generateConfig = ( + projectConfig: ProjectConfig, + selectedPlatforms: string[], + selectedTargets: RunTarget[], + bundleIds: Record +) => { + const imports = []; + if (selectedPlatforms.includes('android')) { + const androidFactories = ['androidPlatform']; + if ( + selectedTargets.some( + (t) => t.platform === 'android' && t.type === 'emulator' + ) + ) + androidFactories.push('androidEmulator'); + if ( + selectedTargets.some( + (t) => t.platform === 'android' && t.type === 'physical' + ) + ) + androidFactories.push('physicalAndroidDevice'); + + imports.push( + `import { ${androidFactories.join( + ', ' + )} } from "@react-native-harness/platform-android";` + ); + } + if (selectedPlatforms.includes('ios')) { + const iosFactories = ['applePlatform']; + if ( + selectedTargets.some((t) => t.platform === 'ios' && t.type === 'emulator') + ) + iosFactories.push('appleSimulator'); + if ( + selectedTargets.some((t) => t.platform === 'ios' && t.type === 'physical') + ) + iosFactories.push('applePhysicalDevice'); + + imports.push( + `import { ${iosFactories.join( + ', ' + )} } from "@react-native-harness/platform-apple";` + ); + } + + const runnerConfigs = selectedTargets.map((target) => { + const platformFn = getPlatformFn(target.platform); + const bundleId = bundleIds[target.platform]; + const name = target.name.toLowerCase().replace(/\s+/g, '-'); + const deviceCall = getDeviceCall(target); + + return ` ${platformFn}({ + name: ${q(name)}, + device: ${deviceCall}, + bundleId: ${q(bundleId)}, + }),`; + }); + + const configContent = ` +${imports.join('\n')} + +export default { + entryPoint: ${q(projectConfig.entryPoint)}, + appRegistryComponentName: ${q(projectConfig.appRegistryComponentName)}, + + runners: [ +${runnerConfigs.join('\n')} + ], + defaultRunner: ${q( + selectedTargets[0].name.toLowerCase().replace(/\s+/g, '-') + )}, +}; +`; + + fs.writeFileSync( + path.join(process.cwd(), 'rn-harness.config.mjs'), + configContent.trim() + '\n' + ); +}; diff --git a/packages/cli/src/wizard/index.ts b/packages/cli/src/wizard/index.ts new file mode 100644 index 0000000..5772b26 --- /dev/null +++ b/packages/cli/src/wizard/index.ts @@ -0,0 +1,72 @@ +import { + intro, + outro, + note, + isProject, + cancelPromptAndExit, + promptConfirm, +} from '@react-native-harness/tools'; +import { getProjectConfig } from './projectType.js'; +import { installPlatforms } from './platforms.js'; +import { discoverTargets } from './targets.js'; +import { getBundleIds } from './bundleId.js'; +import { generateConfig } from './configGenerator.js'; +import { createJestConfig } from './jestConfig.js'; +import fs from 'node:fs'; +import path from 'node:path'; + +const CONFIG_EXTENSIONS = ['.js', '.mjs', '.cjs', '.json']; + +const checkForExistingConfig = async (projectRoot: string) => { + const existingConfig = CONFIG_EXTENSIONS.find((ext) => + fs.existsSync(path.join(projectRoot, `rn-harness.config${ext}`)) + ); + + if (existingConfig) { + const shouldOverwrite = await promptConfirm({ + message: `A configuration file (rn-harness.config${existingConfig}) already exists. Are you sure you want to overwrite it?`, + confirmLabel: 'Overwrite', + cancelLabel: 'Keep existing', + }); + + if (!shouldOverwrite) { + cancelPromptAndExit('Setup cancelled. Keeping existing configuration.'); + } + } +}; + +export const runInitWizard = async () => { + const projectRoot = process.cwd(); + + if (!isProject(projectRoot)) { + cancelPromptAndExit( + 'React Native Harness must be run in a React Native project root (directory with package.json containing react-native).' + ); + } + + intro('React Native Harness'); + + note( + "This wizard will guide you through the setup process to get React Native Harness up and running in your project. We'll help you configure your project type, platforms, and test targets.", + 'Configuration wizard' + ); + + await checkForExistingConfig(projectRoot); + + const projectConfig = await getProjectConfig(); + const selectedPlatforms = await installPlatforms(); + const selectedTargets = await discoverTargets(selectedPlatforms); + const bundleIds = await getBundleIds(selectedPlatforms); + + generateConfig(projectConfig, selectedPlatforms, selectedTargets, bundleIds); + await createJestConfig(projectRoot); + + note( + 'A dedicated Jest configuration (jest.harness.config.mjs) has been created. Harness will automatically use it when you run the "harness" command.', + 'Jest configuration' + ); + + outro( + 'Setup complete. Happy testing!' + ); +}; diff --git a/packages/cli/src/wizard/jestConfig.ts b/packages/cli/src/wizard/jestConfig.ts new file mode 100644 index 0000000..e4b2ab5 --- /dev/null +++ b/packages/cli/src/wizard/jestConfig.ts @@ -0,0 +1,28 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { promptConfirm } from '@react-native-harness/tools'; + +export const createJestConfig = async (projectRoot: string) => { + const configPath = path.join(projectRoot, 'jest.harness.config.mjs'); + + if (fs.existsSync(configPath)) { + const shouldOverwrite = await promptConfirm({ + message: + 'A dedicated Jest configuration (jest.harness.config.mjs) already exists. Do you want to overwrite it?', + confirmLabel: 'Overwrite', + cancelLabel: 'Keep existing', + }); + + if (!shouldOverwrite) { + return; + } + } + + const configContent = `export default { + preset: 'react-native-harness', + testMatch: ['/**/__tests__/**/*.harness.[jt]s?(x)'], +}; +`; + + fs.writeFileSync(configPath, configContent); +}; diff --git a/packages/cli/src/wizard/platforms.ts b/packages/cli/src/wizard/platforms.ts new file mode 100644 index 0000000..0433c00 --- /dev/null +++ b/packages/cli/src/wizard/platforms.ts @@ -0,0 +1,41 @@ +import { + promptMultiselect, + spinner, + cancelPromptAndExit, + installDevDependency, +} from '@react-native-harness/tools'; + +export const installPlatforms = async (): Promise => { + const projectRoot = process.cwd(); + const selectedPlatforms = await promptMultiselect({ + message: 'Select platforms to support', + options: [ + { value: 'android', label: 'Android' }, + { value: 'ios', label: 'iOS' }, + ], + }); + + if (selectedPlatforms.length === 0) { + cancelPromptAndExit('At least one platform must be selected.'); + } + + const installSpinner = spinner(); + installSpinner.start('Installing platform packages...'); + + const packagesToInstall: string[] = []; + if (selectedPlatforms.includes('android')) + packagesToInstall.push('@react-native-harness/platform-android'); + if (selectedPlatforms.includes('ios')) + packagesToInstall.push('@react-native-harness/platform-apple'); + + try { + await installDevDependency(projectRoot, packagesToInstall); + installSpinner.stop('Platform packages installed successfully.'); + } catch (error) { + installSpinner.stop('Failed to install platform packages.', 1); + console.error(error); + process.exit(1); + } + + return selectedPlatforms as string[]; +}; diff --git a/packages/cli/src/wizard/projectType.ts b/packages/cli/src/wizard/projectType.ts new file mode 100644 index 0000000..9825c72 --- /dev/null +++ b/packages/cli/src/wizard/projectType.ts @@ -0,0 +1,77 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { promptSelect, promptText } from '@react-native-harness/tools'; + +export type ProjectConfig = { + entryPoint: string; + appRegistryComponentName: string; +}; + +const tryExtractComponentName = (entryPoint: string): string | undefined => { + try { + const filePath = path.resolve(process.cwd(), entryPoint); + if (!fs.existsSync(filePath)) return undefined; + + const content = fs.readFileSync(filePath, 'utf8'); + // Look for AppRegistry.registerComponent('Name', ...) or AppRegistry.registerComponent("Name", ...) + const match = content.match( + /AppRegistry\.registerComponent\(\s*['"](.+?)['"]/ + ); + + return match ? match[1] : undefined; + } catch { + return undefined; + } +}; + +export const getProjectConfig = async (): Promise => { + const projectType = await promptSelect({ + message: 'What type of project is this?', + options: [ + { value: 'expo', label: 'Expo' }, + { value: 'custom', label: 'React Native CLI / Custom' }, + ], + }); + + if (projectType === 'expo') { + try { + const packageJson = JSON.parse( + fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8') + ); + return { + entryPoint: packageJson.main || './node_modules/expo/AppEntry.js', + appRegistryComponentName: 'main', + }; + } catch { + return { + entryPoint: './node_modules/expo/AppEntry.js', + appRegistryComponentName: 'main', + }; + } + } + + const entryPoint = await promptText({ + message: 'Enter the path to your app entry file', + initialValue: './index.js', + placeholder: './index.js', + validate: (value: string | undefined) => { + if (!value) return 'Entry point is required'; + return; + }, + }); + + const suggestedComponentName = tryExtractComponentName(entryPoint); + + const appRegistryComponentName = await promptText({ + message: + 'Enter the name of the component registered via AppRegistry.registerComponent()', + placeholder: 'MyAppName', + initialValue: suggestedComponentName, + validate: (value: string | undefined) => { + if (!value) return 'Component name is required'; + return; + }, + }); + + return { entryPoint, appRegistryComponentName }; +}; diff --git a/packages/cli/src/wizard/targets.ts b/packages/cli/src/wizard/targets.ts new file mode 100644 index 0000000..995102c --- /dev/null +++ b/packages/cli/src/wizard/targets.ts @@ -0,0 +1,64 @@ +import { + spinner, + promptAutocompleteMultiselect, + cancelPromptAndExit, +} from '@react-native-harness/tools'; +import type { RunTarget } from '@react-native-harness/platforms'; + +export const discoverTargets = async ( + selectedPlatforms: string[] +): Promise => { + const allTargets: RunTarget[] = []; + const targetSpinner = spinner(); + targetSpinner.start('Discovering available targets...'); + + if (selectedPlatforms.includes('android')) { + try { + const androidPlatform = await import( + '@react-native-harness/platform-android' + ); + const targets: RunTarget[] = await androidPlatform.getRunTargets(); + allTargets.push(...targets); + } catch (e) { + console.error('Failed to load Android targets:', e); + } + } + + if (selectedPlatforms.includes('ios')) { + try { + const applePlatform = await import( + '@react-native-harness/platform-apple' + ); + const targets: RunTarget[] = await applePlatform.getRunTargets(); + allTargets.push(...targets); + } catch (e) { + console.error('Failed to load iOS targets:', e); + } + } + + targetSpinner.stop('Target discovery complete.'); + + if (allTargets.length === 0) { + cancelPromptAndExit('No available targets (emulators or devices) found.'); + } + + const options = allTargets.map((target, index) => { + const platformLabel = target.platform === 'android' ? 'Android' : 'iOS'; + return { + value: index, + label: `[${platformLabel}] ${target.name} (${target.type})`, + hint: target.description, + }; + }); + + const selectedTargetIndices = await promptAutocompleteMultiselect({ + message: 'Select targets to support in Harness', + options, + }); + + if (selectedTargetIndices.length === 0) { + cancelPromptAndExit('At least one target must be selected.'); + } + + return selectedTargetIndices.map((i) => allTargets[i]); +}; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index f25b316..e57900f 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,6 +3,18 @@ "files": [], "include": [], "references": [ + { + "path": "../platform-ios" + }, + { + "path": "../platform-android" + }, + { + "path": "../tools" + }, + { + "path": "../platforms" + }, { "path": "../config" }, diff --git a/packages/cli/tsconfig.lib.json b/packages/cli/tsconfig.lib.json index cd8037c..e0c552e 100644 --- a/packages/cli/tsconfig.lib.json +++ b/packages/cli/tsconfig.lib.json @@ -12,6 +12,18 @@ }, "include": ["src/**/*.ts"], "references": [ + { + "path": "../platform-ios/tsconfig.lib.json" + }, + { + "path": "../platform-android/tsconfig.lib.json" + }, + { + "path": "../tools/tsconfig.lib.json" + }, + { + "path": "../platforms/tsconfig.lib.json" + }, { "path": "../config/tsconfig.lib.json" }, diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 48d56c2..c4f2b23 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -122,3 +122,53 @@ export const isAppRunning = async ( ]); return stdout.trim() !== ''; }; + +export const getAvds = async (): Promise => { + try { + const { stdout } = await spawn('emulator', ['-list-avds']); + return stdout + .split('\n') + .map((line) => line.trim()) + .filter((line) => line !== ''); + } catch { + return []; + } +}; + +export type AdbDevice = { + id: string; + model: string; + manufacturer: string; +}; + +export const getConnectedDevices = async (): Promise => { + const { stdout } = await spawn('adb', ['devices', '-l']); + const lines = stdout.split('\n').slice(1); + const devices: AdbDevice[] = []; + + for (const line of lines) { + if (line.trim() === '') continue; + + const parts = line.split(/\s+/); + const id = parts[0]; + + // If it's an emulator, we skip it here as we handle emulators via AVDs + if (id.startsWith('emulator-')) continue; + + // Parse model and manufacturer from 'adb devices -l' output + // Example: 0123456789ABCDEF device usb:337641472X product:sdk_gphone64_arm64 model:Pixel_6 device:oriole transport_id:1 + const modelMatch = line.match(/model:(\S+)/); + const model = modelMatch ? modelMatch[1].replace(/_/g, ' ') : 'Unknown'; + + const manufacturer = + (await getShellProperty(id, 'ro.product.manufacturer')) ?? 'Unknown'; + + devices.push({ + id, + model, + manufacturer, + }); + } + + return devices; +}; diff --git a/packages/platform-android/src/index.ts b/packages/platform-android/src/index.ts index 2d52323..ba6b93c 100644 --- a/packages/platform-android/src/index.ts +++ b/packages/platform-android/src/index.ts @@ -4,3 +4,4 @@ export { androidPlatform, } from './factory.js'; export type { AndroidPlatformConfig } from './config.js'; +export { getRunTargets } from './targets.js'; \ No newline at end of file diff --git a/packages/platform-android/src/targets.ts b/packages/platform-android/src/targets.ts new file mode 100644 index 0000000..694435e --- /dev/null +++ b/packages/platform-android/src/targets.ts @@ -0,0 +1,38 @@ +import { RunTarget } from "@react-native-harness/platforms"; +import * as adb from './adb.js'; + +export const getRunTargets = async (): Promise => { + const [avds, connectedDevices] = await Promise.all([ + adb.getAvds(), + adb.getConnectedDevices(), + ]); + + const targets: RunTarget[] = []; + + for (const avd of avds) { + targets.push({ + type: 'emulator', + name: avd, + platform: 'android', + description: 'Android emulator', + device: { + name: avd, + }, + }); + } + + for (const device of connectedDevices) { + targets.push({ + type: 'physical', + name: `${device.manufacturer} ${device.model}`, + platform: 'android', + description: `Physical device (${device.id})`, + device: { + manufacturer: device.manufacturer, + model: device.model, + }, + }); + } + + return targets; +}; diff --git a/packages/platform-ios/src/index.ts b/packages/platform-ios/src/index.ts index d3a9fc4..54998a0 100644 --- a/packages/platform-ios/src/index.ts +++ b/packages/platform-ios/src/index.ts @@ -4,3 +4,4 @@ export { applePhysicalDevice, } from './factory.js'; export type { ApplePlatformConfig } from './config.js'; +export { getRunTargets } from './targets.js'; \ No newline at end of file diff --git a/packages/platform-ios/src/targets.ts b/packages/platform-ios/src/targets.ts new file mode 100644 index 0000000..dcdcd11 --- /dev/null +++ b/packages/platform-ios/src/targets.ts @@ -0,0 +1,49 @@ +import * as simctl from './xcrun/simctl.js'; +import * as devicectl from './xcrun/devicectl.js'; +import type { RunTarget } from '@react-native-harness/platforms'; + +export const getRunTargets = async (): Promise => { + const [simulators, physicalDevices] = await Promise.all([ + simctl.getSimulators().catch(() => [] as simctl.AppleSimulatorInfo[]), + devicectl.listDevices().catch(() => [] as devicectl.AppleDeviceInfo[]), + ]); + + const targets: RunTarget[] = []; + + for (const simulator of simulators) { + if (!simulator.isAvailable) continue; + + // runtime example: com.apple.CoreSimulator.SimRuntime.iOS-17-5 + const systemVersion = + simulator.runtime + .split('.') + .pop() + ?.replace('iOS-', '') + .replace(/-/g, '.') ?? 'Unknown'; + + targets.push({ + type: 'emulator', + name: simulator.name, + platform: 'ios', + description: `iOS ${systemVersion}`, + device: { + name: simulator.name, + systemVersion: systemVersion, + }, + }); + } + + for (const device of physicalDevices) { + targets.push({ + type: 'physical', + name: device.deviceProperties.name, + platform: 'ios', + description: `Physical device (${device.deviceProperties.osVersionNumber})`, + device: { + name: device.deviceProperties.name, + }, + }); + } + + return targets; +}; diff --git a/packages/platforms/src/index.ts b/packages/platforms/src/index.ts index 6e08ee4..53b0837 100644 --- a/packages/platforms/src/index.ts +++ b/packages/platforms/src/index.ts @@ -1,4 +1,8 @@ -export type { HarnessPlatform, HarnessPlatformRunner } from './types.js'; +export type { + HarnessPlatform, + HarnessPlatformRunner, + RunTarget, +} from './types.js'; export { AppNotInstalledError, DeviceNotFoundError, diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts index b6d3f5f..dd00dc1 100644 --- a/packages/platforms/src/types.ts +++ b/packages/platforms/src/types.ts @@ -11,3 +11,11 @@ export type HarnessPlatform> = { config: TConfig; runner: string; }; + +export type RunTarget = { + type: 'emulator' | 'physical'; + name: string; + platform: string; + description?: string; + device: Record; +}; diff --git a/packages/react-native-harness/package.json b/packages/react-native-harness/package.json index 66dfcce..5d7433c 100644 --- a/packages/react-native-harness/package.json +++ b/packages/react-native-harness/package.json @@ -2,7 +2,8 @@ "name": "react-native-harness", "version": "1.0.0-alpha.23", "bin": { - "react-native-harness": "./bin.js" + "react-native-harness": "./bin.js", + "harness": "./bin.js" }, "type": "module", "main": "./dist/index.js", diff --git a/packages/tools/package.json b/packages/tools/package.json index 3f28662..69a982a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -15,7 +15,7 @@ } }, "dependencies": { - "@clack/prompts": "1.0.0-alpha.5", + "@clack/prompts": "1.0.0-alpha.9", "is-unicode-supported": "^0.1.0", "nano-spawn": "^1.0.2", "picocolors": "^1.1.1", diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index e3d666d..3530c80 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -6,3 +6,4 @@ export * from './spawn.js'; export * from './react-native.js'; export * from './error.js'; export * from './events.js'; +export * from './packages.js'; \ No newline at end of file diff --git a/packages/tools/src/packages.ts b/packages/tools/src/packages.ts new file mode 100644 index 0000000..150ac7d --- /dev/null +++ b/packages/tools/src/packages.ts @@ -0,0 +1,73 @@ +import path from 'node:path'; +import fs from 'node:fs'; +import { spawn } from './spawn.js'; + +const getPackageManager = (): string => { + const packageManager = process.env.npm_config_user_agent; + + if (packageManager?.startsWith('pnpm')) { + return 'pnpm'; + } + + if (packageManager?.startsWith('yarn')) { + return 'yarn'; + } + + if (packageManager?.startsWith('bun')) { + return 'bun'; + } + + return 'npm'; +}; + +export const getExecForPackageManager = (): string => { + const packageManager = getPackageManager(); + + if (packageManager === 'pnpm') { + return 'pnpx'; + } else if (packageManager === 'yarn') { + return 'yarn dlx'; + } else if (packageManager === 'bun') { + return 'bunx'; + } + + return 'npx'; +}; + +export const installDependencies = async ( + projectRoot: string +): Promise => { + const packageManager = getPackageManager(); + await spawn(packageManager, ['install'], { cwd: projectRoot }); +}; + +export const installDevDependency = async ( + projectRoot: string, + packageName: string | string[] +): Promise => { + const packageManager = getPackageManager(); + const isNpm = packageManager === 'npm'; + const installCmd = isNpm ? 'install' : 'add'; + const packages = Array.isArray(packageName) ? packageName : [packageName]; + const args = [installCmd, '-D', ...packages]; + await spawn(packageManager, args, { cwd: projectRoot }); +}; + +export const isPackageInstalled = async ( + projectRoot: string, + packageName: string +): Promise => { + const packageManager = getPackageManager(); + const args = ['list', '--depth=0', '--json']; + const process = await spawn(packageManager, args, { cwd: projectRoot }); + const output = process.output; + return output.includes(packageName); +}; + +export const isProject = (projectRoot: string): boolean => { + const packageJsonPath = path.join(projectRoot, 'package.json'); + return ( + fs.existsSync(packageJsonPath) && + fs.readFileSync(packageJsonPath, 'utf8').includes('react-native') + ); +}; diff --git a/packages/tools/src/prompts.ts b/packages/tools/src/prompts.ts index 6449b5c..1b12b11 100644 --- a/packages/tools/src/prompts.ts +++ b/packages/tools/src/prompts.ts @@ -82,6 +82,28 @@ export const promptMultiselect = async ( return result as T[]; }; +export const promptGroupMultiselect = async ( + options: clack.GroupMultiSelectOptions +): Promise => { + const result = await clack.groupMultiselect(options); + if (clack.isCancel(result)) { + cancelPromptAndExit(); + } + + return result as T[]; +}; + +export const promptAutocompleteMultiselect = async ( + options: clack.AutocompleteMultiSelectOptions +): Promise => { + const result = await clack.autocompleteMultiselect(options); + if (clack.isCancel(result)) { + cancelPromptAndExit(); + } + + return result as T[]; +}; + export const promptGroup = async ( prompts: clack.PromptGroup, options?: clack.PromptGroupOptions | undefined @@ -111,8 +133,12 @@ export const spinner = (options?: clack.SpinnerOptions) => { start: (message?: string) => { clackSpinner.start(message); }, - stop: (message?: string, code?: number) => { - clackSpinner.stop(message, code); + stop: (message?: string, code = 0) => { + if (code === 0) { + clackSpinner.stop(message); + } else { + clackSpinner.error(message); + } }, message: (message?: string) => { clackSpinner.message(message); @@ -144,7 +170,11 @@ export const progress = (options?: clack.ProgressOptions) => { clackProgress.advance(value, message); }, stop: (message?: string, code = 0) => { - clackProgress.stop(message, code); + if (code === 0) { + clackProgress.stop(message); + } else { + clackProgress.error(message); + } }, message: (message?: string) => { clackProgress.message(message); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1de4b9b..31020e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,13 +31,13 @@ importers: version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/react-native': specifier: 22.0.4 - version: 22.0.4(b6bdbf441fca35f932c3e030670387b0) + version: 22.0.4(j5mvkpom3exmbpfctoxl2tygky) '@nx/rollup': specifier: 22.0.4 version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3) '@nx/vite': specifier: 22.0.4 - version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4) + version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0)) '@nx/web': specifier: 22.0.4 version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -64,7 +64,7 @@ importers: version: 4.6.0(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0)) '@vitest/coverage-v8': specifier: ^3.0.5 - version: 3.2.4(vitest@3.2.4) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0)) '@vitest/ui': specifier: ^3.0.0 version: 3.2.4(vitest@3.2.4) @@ -273,10 +273,22 @@ importers: '@react-native-harness/config': specifier: workspace:* version: link:../config + '@react-native-harness/platforms': + specifier: workspace:* + version: link:../platforms + '@react-native-harness/tools': + specifier: workspace:* + version: link:../tools tslib: specifier: ^2.3.0 version: 2.8.1 devDependencies: + '@react-native-harness/platform-android': + specifier: workspace:* + version: link:../platform-android + '@react-native-harness/platform-apple': + specifier: workspace:* + version: link:../platform-ios jest-cli: specifier: ^30.2.0 version: 30.2.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0)(ts-node@9.1.1(typescript@5.9.3)) @@ -493,8 +505,8 @@ importers: packages/tools: dependencies: '@clack/prompts': - specifier: 1.0.0-alpha.5 - version: 1.0.0-alpha.5 + specifier: 1.0.0-alpha.9 + version: 1.0.0-alpha.9 is-unicode-supported: specifier: ^0.1.0 version: 0.1.0 @@ -1258,11 +1270,11 @@ packages: '@rspress/core': ^2.0.0-beta.32 react: ^19.0.0 - '@clack/core@1.0.0-alpha.5': - resolution: {integrity: sha512-z02wRlW7F7L5N5r2otDMsrLNKAgjDIDD+m4do5/cBqiYCKKb7SNBJgpUISjuCmRI0/P5XyoInmMrr1rBoH8MKw==} + '@clack/core@1.0.0-alpha.7': + resolution: {integrity: sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ==} - '@clack/prompts@1.0.0-alpha.5': - resolution: {integrity: sha512-hY67bxfwwti2WkLOLOiuXtAdD43KNMV4yiJPPSEdZG8N5TfZ9lLxibmqwKe5UZ5364PIp4kzTEvge/4Crcd5bg==} + '@clack/prompts@1.0.0-alpha.9': + resolution: {integrity: sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==} '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} @@ -9731,14 +9743,14 @@ snapshots: '@rspress/core': 2.0.0-beta.32(@types/react@19.1.13) react: 19.1.1 - '@clack/core@1.0.0-alpha.5': + '@clack/core@1.0.0-alpha.7': dependencies: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/prompts@1.0.0-alpha.5': + '@clack/prompts@1.0.0-alpha.9': dependencies: - '@clack/core': 1.0.0-alpha.5 + '@clack/core': 1.0.0-alpha.7 picocolors: 1.1.1 sisteransi: 1.0.5 @@ -10562,13 +10574,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nx/detox@22.0.4(9b1a4b49f421cf013b90dafab0a49a31)': + '@nx/detox@22.0.4(htxksnrdasuzrfoxdvkmoikuza)': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/jest': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/node@20.19.25)(babel-plugin-macros@3.1.0)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) - '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) detox: 20.46.0(@jest/environment@30.2.0)(@jest/types@30.2.0)(expect@30.2.0)(jest-environment-node@30.2.0)(jest@30.2.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0)(ts-node@9.1.1(typescript@5.9.3))) tslib: 2.8.1 transitivePeerDependencies: @@ -10794,12 +10806,12 @@ snapshots: '@nx/nx-win32-x64-msvc@22.0.4': optional: true - '@nx/react-native@22.0.4(b6bdbf441fca35f932c3e030670387b0)': + '@nx/react-native@22.0.4(j5mvkpom3exmbpfctoxl2tygky)': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) - '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) ajv: 8.17.1 enhanced-resolve: 5.18.3 ignore: 5.3.2 @@ -10810,7 +10822,7 @@ snapshots: tsconfig-paths: 4.2.0 tslib: 2.8.1 optionalDependencies: - '@nx/detox': 22.0.4(9b1a4b49f421cf013b90dafab0a49a31) + '@nx/detox': 22.0.4(htxksnrdasuzrfoxdvkmoikuza) '@nx/rollup': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3) transitivePeerDependencies: - '@babel/core' @@ -10845,7 +10857,7 @@ snapshots: - webpack - webpack-cli - '@nx/react@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/react@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(ts-node@9.1.1(typescript@5.9.3))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -10863,7 +10875,7 @@ snapshots: semver: 7.7.2 tslib: 2.8.1 optionalDependencies: - '@nx/vite': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4) + '@nx/vite': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0)) transitivePeerDependencies: - '@babel/core' - '@babel/traverse' @@ -10924,7 +10936,7 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)': + '@nx/vite@22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -12399,7 +12411,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(less@4.1.3)(lightningcss@1.27.0)(sass-embedded@1.89.2)(sass@1.89.2)(stylus@0.64.0)(terser@5.42.0)(yaml@2.8.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 From 5d2c402339083f50385a0bc341b2f89ec0d70d61 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 17:18:54 +0100 Subject: [PATCH 2/5] chore: add version plan --- .nx/version-plans/version-plan-1769012201799.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .nx/version-plans/version-plan-1769012201799.md diff --git a/.nx/version-plans/version-plan-1769012201799.md b/.nx/version-plans/version-plan-1769012201799.md new file mode 100644 index 0000000..13fdd82 --- /dev/null +++ b/.nx/version-plans/version-plan-1769012201799.md @@ -0,0 +1,6 @@ +--- +"@react-native-harness/cli": prerelease +--- + +Add interactive Harness init wizard to guide users through setup and config. + From c32228c8b4daaae26b9c92609a86e3293fec287c Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 17:27:21 +0100 Subject: [PATCH 3/5] docs: update website --- .../src/docs/getting-started/quick-start.mdx | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/website/src/docs/getting-started/quick-start.mdx b/website/src/docs/getting-started/quick-start.mdx index 9ed9310..ba9e34c 100644 --- a/website/src/docs/getting-started/quick-start.mdx +++ b/website/src/docs/getting-started/quick-start.mdx @@ -18,13 +18,42 @@ Install React Native Harness as a development dependency: ## Configuration -### 1. Install Platform Packages +The easiest way to get started is with our interactive setup wizard! It will guide you through the configuration process and handle most of the setup automatically. + +Run the wizard from your project root: + + + +The wizard will: +- Ask for your project type (Expo or React Native CLI) +- Help you select platforms (Android/iOS) +- Find available devices and simulators +- Generate bundle IDs and create configuration files +- Install required platform packages +- Set up Jest configuration + +:::tip Wizard Benefits +The wizard handles the complex configuration details for you, making setup much faster and less error-prone than manual configuration. +::: + +### Manual Configuration (if wizard fails) + +If the wizard doesn't work for your setup, you can configure Harness manually: + +#### 1. Install Platform Packages -First, install the platform packages you need: +Install the platform packages you need: -### 2. Create Harness Configuration +#### 2. Create Harness Configuration Create a `rn-harness.config.mjs` file in your project root: @@ -67,9 +96,9 @@ The `entryPoint` and `appRegistryComponentName` properties tell React Native Har For Expo projects, the `entryPoint` should be set to the path specified in the `main` property of package.json. The `appRegistryComponentName` is typically set to `main` for Expo apps. ::: -### 3. Update Metro Configuration +### 3. Update Metro Configuration (Required) -Update your `metro.config.js` so React Native Harness will be able to use it to bundle its tests: +Whether you use the wizard or manual setup, you still need to update your Metro configuration so React Native Harness can bundle its tests: ```javascript const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); @@ -88,9 +117,9 @@ module.exports = withRnHarness(mergeConfig(defaultConfig, customConfig)); The `withRnHarness` function is a noop when you're not running Harness tests, so you don't need to worry about it affecting your app in production. It only kicks in when running tests! ::: -### 4. Configure Jest +#### 3. Configure Jest -Update your `jest.config.js` to use the React Native Harness preset: +The wizard creates a dedicated `jest.harness.config.mjs` file for your Harness tests. If you prefer manual setup, update your `jest.config.js`: ```javascript module.exports = { @@ -99,7 +128,7 @@ module.exports = { ``` :::tip Running Both Classic and Harness Tests -If you want to run both traditional Jest tests and Harness tests in the same app, you can use Jest's `projects` feature. This lets you separate your regular unit tests from your in-app Harness tests: +If you want to run both traditional Jest tests and Harness tests in the same app, you can use Jest's `projects` feature. The wizard creates a separate Jest config for Harness tests, but you can also configure this manually: ```javascript module.exports = { From 04a88011f11237d7f03bf5930ee3aa982817b774 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 21 Jan 2026 17:47:40 +0100 Subject: [PATCH 4/5] chore: minor tweaks --- apps/playground/jest.harness.config.mjs | 6 ++++++ packages/platform-android/src/factory.ts | 2 +- website/src/docs/getting-started/quick-start.mdx | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/playground/jest.harness.config.mjs b/apps/playground/jest.harness.config.mjs index c9e6f90..10d7100 100644 --- a/apps/playground/jest.harness.config.mjs +++ b/apps/playground/jest.harness.config.mjs @@ -1,4 +1,10 @@ export default { preset: 'react-native-harness', testMatch: ['/**/__tests__/**/*.harness.[jt]s?(x)'], + setupFiles: ['./src/setupFile.ts'], + setupFilesAfterEnv: ['./src/setupFileAfterEnv.ts'], + // This is necessary to prevent Jest from transforming the workspace packages. + // Not needed in users projects, as they will have the packages installed in their node_modules. + transformIgnorePatterns: ['/packages/', '/node_modules/'], + collectCoverageFrom: ['./src/**/*.(ts|tsx)'], }; diff --git a/packages/platform-android/src/factory.ts b/packages/platform-android/src/factory.ts index 1f16a59..d136e4c 100644 --- a/packages/platform-android/src/factory.ts +++ b/packages/platform-android/src/factory.ts @@ -27,7 +27,7 @@ export const physicalAndroidDevice = ( export const androidPlatform = ( config: AndroidPlatformConfig ): HarnessPlatform => ({ - name: 'android', + name: config.name, config, runner: import.meta.resolve('./runner.js'), }); diff --git a/website/src/docs/getting-started/quick-start.mdx b/website/src/docs/getting-started/quick-start.mdx index ba9e34c..ccf6b9e 100644 --- a/website/src/docs/getting-started/quick-start.mdx +++ b/website/src/docs/getting-started/quick-start.mdx @@ -119,10 +119,10 @@ The `withRnHarness` function is a noop when you're not running Harness tests, so #### 3. Configure Jest -The wizard creates a dedicated `jest.harness.config.mjs` file for your Harness tests. If you prefer manual setup, update your `jest.config.js`: +The wizard creates a dedicated `jest.harness.config.mjs` file for your Harness tests. If you prefer manual setup, create a `jest.harness.config.mjs` file: ```javascript -module.exports = { +export default { preset: 'react-native-harness', }; ``` From 95ace0995f41a6b4f942435dee339e3045882d20 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 23 Jan 2026 11:53:28 +0100 Subject: [PATCH 5/5] feat: add web --- packages/cli/eslint.config.mjs | 2 +- packages/cli/package.json | 3 +- packages/cli/src/wizard/bundleId.ts | 17 ++ packages/cli/src/wizard/configGenerator.ts | 31 ++- packages/cli/src/wizard/platforms.ts | 3 + packages/cli/src/wizard/targets.ts | 16 +- packages/cli/tsconfig.json | 11 +- packages/cli/tsconfig.lib.json | 11 +- packages/platform-web/src/index.ts | 1 + packages/platform-web/src/targets.ts | 27 ++ packages/platforms/src/types.ts | 2 +- pnpm-lock.yaml | 279 +++++++++++---------- 12 files changed, 257 insertions(+), 146 deletions(-) create mode 100644 packages/platform-web/src/targets.ts diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs index 54c1935..468f788 100644 --- a/packages/cli/eslint.config.mjs +++ b/packages/cli/eslint.config.mjs @@ -9,7 +9,7 @@ export default [ 'error', { ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], - ignoredDependencies: ['@react-native-harness/bridge', '@react-native-harness/platform-android', '@react-native-harness/platform-apple'], + ignoredDependencies: ['@react-native-harness/bridge', '@react-native-harness/platform-android', '@react-native-harness/platform-apple', '@react-native-harness/platform-web'], }, ], }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 3b6440d..9f8afcb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,7 +30,8 @@ "devDependencies": { "jest-cli": "^30.2.0", "@react-native-harness/platform-android": "workspace:*", - "@react-native-harness/platform-apple": "workspace:*" + "@react-native-harness/platform-apple": "workspace:*", + "@react-native-harness/platform-web": "workspace:*" }, "peerDependencies": { "jest-cli": "*" diff --git a/packages/cli/src/wizard/bundleId.ts b/packages/cli/src/wizard/bundleId.ts index 549714b..156a874 100644 --- a/packages/cli/src/wizard/bundleId.ts +++ b/packages/cli/src/wizard/bundleId.ts @@ -49,5 +49,22 @@ export const getBundleIds = async ( }); } + if (selectedPlatforms.includes('web')) { + bundleIds.web = await promptText({ + message: 'Enter application URL', + initialValue: 'http://localhost:8081/index.html', + placeholder: 'http://localhost:8081/index.html', + validate: (value: string | undefined) => { + if (!value) return 'URL is required'; + try { + new URL(value); + return; + } catch (e) { + return 'Please enter a valid URL'; + } + }, + }); + } + return bundleIds; }; diff --git a/packages/cli/src/wizard/configGenerator.ts b/packages/cli/src/wizard/configGenerator.ts index cdbff84..75dc3ba 100644 --- a/packages/cli/src/wizard/configGenerator.ts +++ b/packages/cli/src/wizard/configGenerator.ts @@ -36,7 +36,7 @@ export const generateConfig = ( selectedTargets: RunTarget[], bundleIds: Record ) => { - const imports = []; + const imports: string[] = []; if (selectedPlatforms.includes('android')) { const androidFactories = ['androidPlatform']; if ( @@ -75,11 +75,38 @@ export const generateConfig = ( )} } from "@react-native-harness/platform-apple";` ); } + if (selectedPlatforms.includes('web')) { + const webFactories = ['webPlatform']; + const browsers = new Set( + selectedTargets + .filter((t) => t.platform === 'web') + .map((t) => t.device.browserType) + ); + for (const browser of browsers) { + if (browser) webFactories.push(browser); + } + + imports.push( + `import { ${webFactories.join( + ', ' + )} } from "@react-native-harness/platform-web";` + ); + } const runnerConfigs = selectedTargets.map((target) => { const platformFn = getPlatformFn(target.platform); - const bundleId = bundleIds[target.platform]; const name = target.name.toLowerCase().replace(/\s+/g, '-'); + + if (target.platform === 'web') { + const url = bundleIds[target.platform]; + const browserCall = `${target.device.browserType}(${q(url)})`; + return ` ${platformFn}({ + name: ${q(name)}, + browser: ${browserCall}, + }),`; + } + + const bundleId = bundleIds[target.platform]; const deviceCall = getDeviceCall(target); return ` ${platformFn}({ diff --git a/packages/cli/src/wizard/platforms.ts b/packages/cli/src/wizard/platforms.ts index 0433c00..b472608 100644 --- a/packages/cli/src/wizard/platforms.ts +++ b/packages/cli/src/wizard/platforms.ts @@ -12,6 +12,7 @@ export const installPlatforms = async (): Promise => { options: [ { value: 'android', label: 'Android' }, { value: 'ios', label: 'iOS' }, + { value: 'web', label: 'Web' }, ], }); @@ -27,6 +28,8 @@ export const installPlatforms = async (): Promise => { packagesToInstall.push('@react-native-harness/platform-android'); if (selectedPlatforms.includes('ios')) packagesToInstall.push('@react-native-harness/platform-apple'); + if (selectedPlatforms.includes('web')) + packagesToInstall.push('@react-native-harness/platform-web'); try { await installDevDependency(projectRoot, packagesToInstall); diff --git a/packages/cli/src/wizard/targets.ts b/packages/cli/src/wizard/targets.ts index 995102c..2d296a0 100644 --- a/packages/cli/src/wizard/targets.ts +++ b/packages/cli/src/wizard/targets.ts @@ -36,6 +36,16 @@ export const discoverTargets = async ( } } + if (selectedPlatforms.includes('web')) { + try { + const webPlatform = await import('@react-native-harness/platform-web'); + const targets: RunTarget[] = await webPlatform.getRunTargets(); + allTargets.push(...targets); + } catch (e) { + console.error('Failed to load Web targets:', e); + } + } + targetSpinner.stop('Target discovery complete.'); if (allTargets.length === 0) { @@ -43,7 +53,11 @@ export const discoverTargets = async ( } const options = allTargets.map((target, index) => { - const platformLabel = target.platform === 'android' ? 'Android' : 'iOS'; + let platformLabel = 'Unknown'; + if (target.platform === 'android') platformLabel = 'Android'; + else if (target.platform === 'ios') platformLabel = 'iOS'; + else if (target.platform === 'web') platformLabel = 'Web'; + return { value: index, label: `[${platformLabel}] ${target.name} (${target.type})`, diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index e57900f..7e4d9db 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -4,10 +4,7 @@ "include": [], "references": [ { - "path": "../platform-ios" - }, - { - "path": "../platform-android" + "path": "../platform-web" }, { "path": "../tools" @@ -21,6 +18,12 @@ { "path": "../bridge" }, + { + "path": "../platform-ios" + }, + { + "path": "../platform-android" + }, { "path": "./tsconfig.lib.json" } diff --git a/packages/cli/tsconfig.lib.json b/packages/cli/tsconfig.lib.json index e0c552e..5f75641 100644 --- a/packages/cli/tsconfig.lib.json +++ b/packages/cli/tsconfig.lib.json @@ -13,10 +13,7 @@ "include": ["src/**/*.ts"], "references": [ { - "path": "../platform-ios/tsconfig.lib.json" - }, - { - "path": "../platform-android/tsconfig.lib.json" + "path": "../platform-web/tsconfig.lib.json" }, { "path": "../tools/tsconfig.lib.json" @@ -29,6 +26,12 @@ }, { "path": "../bridge/tsconfig.lib.json" + }, + { + "path": "../platform-ios/tsconfig.lib.json" + }, + { + "path": "../platform-android/tsconfig.lib.json" } ] } diff --git a/packages/platform-web/src/index.ts b/packages/platform-web/src/index.ts index 05938be..5651a53 100644 --- a/packages/platform-web/src/index.ts +++ b/packages/platform-web/src/index.ts @@ -1,2 +1,3 @@ export { webPlatform, chromium, chrome, firefox, webkit } from './factory.js'; +export { getRunTargets } from './targets.js'; export type { WebPlatformConfig } from './config.js'; diff --git a/packages/platform-web/src/targets.ts b/packages/platform-web/src/targets.ts new file mode 100644 index 0000000..2e9a20c --- /dev/null +++ b/packages/platform-web/src/targets.ts @@ -0,0 +1,27 @@ +import { RunTarget } from '@react-native-harness/platforms'; + +export const getRunTargets = async (): Promise => { + return [ + { + type: 'browser', + name: 'Chromium', + platform: 'web', + description: 'Playwright Chromium browser', + device: { browserType: 'chromium' }, + }, + { + type: 'browser', + name: 'Firefox', + platform: 'web', + description: 'Playwright Firefox browser', + device: { browserType: 'firefox' }, + }, + { + type: 'browser', + name: 'WebKit', + platform: 'web', + description: 'Playwright WebKit browser', + device: { browserType: 'webkit' }, + }, + ]; +}; diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts index dd00dc1..d5c2e75 100644 --- a/packages/platforms/src/types.ts +++ b/packages/platforms/src/types.ts @@ -13,7 +13,7 @@ export type HarnessPlatform> = { }; export type RunTarget = { - type: 'emulator' | 'physical'; + type: 'emulator' | 'physical' | 'browser'; name: string; platform: string; description?: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa4931f..f165728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,13 +31,13 @@ importers: version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/react-native': specifier: 22.0.4 - version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/rollup': specifier: 22.0.4 version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3) '@nx/vite': specifier: 22.0.4 - version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0)) + version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4) '@nx/web': specifier: 22.0.4 version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -64,7 +64,7 @@ importers: version: 4.6.0(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0)) '@vitest/coverage-v8': specifier: ^3.0.5 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0)) + version: 3.2.4(vitest@3.2.4) '@vitest/ui': specifier: ^3.0.0 version: 3.2.4(vitest@3.2.4) @@ -282,10 +282,25 @@ importers: '@react-native-harness/config': specifier: workspace:* version: link:../config + '@react-native-harness/platforms': + specifier: workspace:* + version: link:../platforms + '@react-native-harness/tools': + specifier: workspace:* + version: link:../tools tslib: specifier: ^2.3.0 version: 2.8.1 devDependencies: + '@react-native-harness/platform-android': + specifier: workspace:* + version: link:../platform-android + '@react-native-harness/platform-apple': + specifier: workspace:* + version: link:../platform-ios + '@react-native-harness/platform-web': + specifier: workspace:* + version: link:../platform-web jest-cli: specifier: ^30.2.0 version: 30.2.0(@types/node@20.19.25)(babel-plugin-macros@3.1.0) @@ -511,8 +526,8 @@ importers: packages/tools: dependencies: '@clack/prompts': - specifier: 1.0.0-alpha.5 - version: 1.0.0-alpha.5 + specifier: 1.0.0-alpha.9 + version: 1.0.0-alpha.9 is-unicode-supported: specifier: ^0.1.0 version: 0.1.0 @@ -1271,11 +1286,11 @@ packages: react: ^19.2.0 react-dom: ^19.2.0 - '@clack/core@1.0.0-alpha.5': - resolution: {integrity: sha512-z02wRlW7F7L5N5r2otDMsrLNKAgjDIDD+m4do5/cBqiYCKKb7SNBJgpUISjuCmRI0/P5XyoInmMrr1rBoH8MKw==} + '@clack/core@1.0.0-alpha.7': + resolution: {integrity: sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ==} - '@clack/prompts@1.0.0-alpha.5': - resolution: {integrity: sha512-hY67bxfwwti2WkLOLOiuXtAdD43KNMV4yiJPPSEdZG8N5TfZ9lLxibmqwKe5UZ5364PIp4kzTEvge/4Crcd5bg==} + '@clack/prompts@1.0.0-alpha.9': + resolution: {integrity: sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==} '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -1292,8 +1307,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.0': - resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -1304,8 +1319,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.0': - resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -1316,8 +1331,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.0': - resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -1328,8 +1343,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.0': - resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -1340,8 +1355,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.0': - resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -1352,8 +1367,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.0': - resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -1364,8 +1379,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.0': - resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -1376,8 +1391,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.0': - resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -1388,8 +1403,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.0': - resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -1400,8 +1415,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.0': - resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -1412,8 +1427,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.0': - resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -1424,8 +1439,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.0': - resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -1436,8 +1451,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.0': - resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -1448,8 +1463,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.0': - resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -1460,8 +1475,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.0': - resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -1472,8 +1487,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.0': - resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -1484,8 +1499,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.0': - resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -1496,8 +1511,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.0': - resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -1508,8 +1523,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.0': - resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -1520,8 +1535,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.0': - resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -1532,14 +1547,14 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.0': - resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.0': - resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -1550,8 +1565,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.0': - resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -1562,8 +1577,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.0': - resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -1574,8 +1589,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.0': - resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -1586,8 +1601,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.0': - resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -4479,8 +4494,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.0: - resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} engines: {node: '>=18'} hasBin: true @@ -9222,14 +9237,14 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@clack/core@1.0.0-alpha.5': + '@clack/core@1.0.0-alpha.7': dependencies: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/prompts@1.0.0-alpha.5': + '@clack/prompts@1.0.0-alpha.9': dependencies: - '@clack/core': 1.0.0-alpha.5 + '@clack/core': 1.0.0-alpha.7 picocolors: 1.1.1 sisteransi: 1.0.5 @@ -9249,154 +9264,154 @@ snapshots: '@esbuild/aix-ppc64@0.25.5': optional: true - '@esbuild/aix-ppc64@0.27.0': + '@esbuild/aix-ppc64@0.27.2': optional: true '@esbuild/android-arm64@0.25.5': optional: true - '@esbuild/android-arm64@0.27.0': + '@esbuild/android-arm64@0.27.2': optional: true '@esbuild/android-arm@0.25.5': optional: true - '@esbuild/android-arm@0.27.0': + '@esbuild/android-arm@0.27.2': optional: true '@esbuild/android-x64@0.25.5': optional: true - '@esbuild/android-x64@0.27.0': + '@esbuild/android-x64@0.27.2': optional: true '@esbuild/darwin-arm64@0.25.5': optional: true - '@esbuild/darwin-arm64@0.27.0': + '@esbuild/darwin-arm64@0.27.2': optional: true '@esbuild/darwin-x64@0.25.5': optional: true - '@esbuild/darwin-x64@0.27.0': + '@esbuild/darwin-x64@0.27.2': optional: true '@esbuild/freebsd-arm64@0.25.5': optional: true - '@esbuild/freebsd-arm64@0.27.0': + '@esbuild/freebsd-arm64@0.27.2': optional: true '@esbuild/freebsd-x64@0.25.5': optional: true - '@esbuild/freebsd-x64@0.27.0': + '@esbuild/freebsd-x64@0.27.2': optional: true '@esbuild/linux-arm64@0.25.5': optional: true - '@esbuild/linux-arm64@0.27.0': + '@esbuild/linux-arm64@0.27.2': optional: true '@esbuild/linux-arm@0.25.5': optional: true - '@esbuild/linux-arm@0.27.0': + '@esbuild/linux-arm@0.27.2': optional: true '@esbuild/linux-ia32@0.25.5': optional: true - '@esbuild/linux-ia32@0.27.0': + '@esbuild/linux-ia32@0.27.2': optional: true '@esbuild/linux-loong64@0.25.5': optional: true - '@esbuild/linux-loong64@0.27.0': + '@esbuild/linux-loong64@0.27.2': optional: true '@esbuild/linux-mips64el@0.25.5': optional: true - '@esbuild/linux-mips64el@0.27.0': + '@esbuild/linux-mips64el@0.27.2': optional: true '@esbuild/linux-ppc64@0.25.5': optional: true - '@esbuild/linux-ppc64@0.27.0': + '@esbuild/linux-ppc64@0.27.2': optional: true '@esbuild/linux-riscv64@0.25.5': optional: true - '@esbuild/linux-riscv64@0.27.0': + '@esbuild/linux-riscv64@0.27.2': optional: true '@esbuild/linux-s390x@0.25.5': optional: true - '@esbuild/linux-s390x@0.27.0': + '@esbuild/linux-s390x@0.27.2': optional: true '@esbuild/linux-x64@0.25.5': optional: true - '@esbuild/linux-x64@0.27.0': + '@esbuild/linux-x64@0.27.2': optional: true '@esbuild/netbsd-arm64@0.25.5': optional: true - '@esbuild/netbsd-arm64@0.27.0': + '@esbuild/netbsd-arm64@0.27.2': optional: true '@esbuild/netbsd-x64@0.25.5': optional: true - '@esbuild/netbsd-x64@0.27.0': + '@esbuild/netbsd-x64@0.27.2': optional: true '@esbuild/openbsd-arm64@0.25.5': optional: true - '@esbuild/openbsd-arm64@0.27.0': + '@esbuild/openbsd-arm64@0.27.2': optional: true '@esbuild/openbsd-x64@0.25.5': optional: true - '@esbuild/openbsd-x64@0.27.0': + '@esbuild/openbsd-x64@0.27.2': optional: true - '@esbuild/openharmony-arm64@0.27.0': + '@esbuild/openharmony-arm64@0.27.2': optional: true '@esbuild/sunos-x64@0.25.5': optional: true - '@esbuild/sunos-x64@0.27.0': + '@esbuild/sunos-x64@0.27.2': optional: true '@esbuild/win32-arm64@0.25.5': optional: true - '@esbuild/win32-arm64@0.27.0': + '@esbuild/win32-arm64@0.27.2': optional: true '@esbuild/win32-ia32@0.25.5': optional: true - '@esbuild/win32-ia32@0.27.0': + '@esbuild/win32-ia32@0.27.2': optional: true '@esbuild/win32-x64@0.25.5': optional: true - '@esbuild/win32-x64@0.27.0': + '@esbuild/win32-x64@0.27.2': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.4.2))': @@ -10066,13 +10081,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nx/detox@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/detox@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/jest': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/node@20.19.25)(babel-plugin-macros@3.1.0)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) - '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) tslib: 2.8.1 transitivePeerDependencies: - '@babel/core' @@ -10297,12 +10312,12 @@ snapshots: '@nx/nx-win32-x64-msvc@22.0.4': optional: true - '@nx/react-native@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/react-native@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) - '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) ajv: 8.17.1 enhanced-resolve: 5.18.3 ignore: 5.3.2 @@ -10313,7 +10328,7 @@ snapshots: tsconfig-paths: 4.2.0 tslib: 2.8.1 optionalDependencies: - '@nx/detox': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/detox': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/rollup': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3) transitivePeerDependencies: - '@babel/core' @@ -10348,7 +10363,7 @@ snapshots: - webpack - webpack-cli - '@nx/react@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/react@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -10366,7 +10381,7 @@ snapshots: semver: 7.7.2 tslib: 2.8.1 optionalDependencies: - '@nx/vite': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0)) + '@nx/vite': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - '@babel/core' - '@babel/traverse' @@ -10427,7 +10442,7 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))': + '@nx/vite@22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -11877,7 +11892,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -12532,9 +12547,9 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bundle-require@5.1.0(esbuild@0.27.0): + bundle-require@5.1.0(esbuild@0.27.2): dependencies: - esbuild: 0.27.0 + esbuild: 0.27.2 load-tsconfig: 0.2.5 bytes@3.1.2: {} @@ -13320,34 +13335,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 - esbuild@0.27.0: + esbuild@0.27.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.0 - '@esbuild/android-arm': 0.27.0 - '@esbuild/android-arm64': 0.27.0 - '@esbuild/android-x64': 0.27.0 - '@esbuild/darwin-arm64': 0.27.0 - '@esbuild/darwin-x64': 0.27.0 - '@esbuild/freebsd-arm64': 0.27.0 - '@esbuild/freebsd-x64': 0.27.0 - '@esbuild/linux-arm': 0.27.0 - '@esbuild/linux-arm64': 0.27.0 - '@esbuild/linux-ia32': 0.27.0 - '@esbuild/linux-loong64': 0.27.0 - '@esbuild/linux-mips64el': 0.27.0 - '@esbuild/linux-ppc64': 0.27.0 - '@esbuild/linux-riscv64': 0.27.0 - '@esbuild/linux-s390x': 0.27.0 - '@esbuild/linux-x64': 0.27.0 - '@esbuild/netbsd-arm64': 0.27.0 - '@esbuild/netbsd-x64': 0.27.0 - '@esbuild/openbsd-arm64': 0.27.0 - '@esbuild/openbsd-x64': 0.27.0 - '@esbuild/openharmony-arm64': 0.27.0 - '@esbuild/sunos-x64': 0.27.0 - '@esbuild/win32-arm64': 0.27.0 - '@esbuild/win32-ia32': 0.27.0 - '@esbuild/win32-x64': 0.27.0 + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escalade@3.2.0: {} @@ -17799,12 +17814,12 @@ snapshots: tsup@8.5.1(@swc/core@1.5.29(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.0): dependencies: - bundle-require: 5.1.0(esbuild@0.27.0) + bundle-require: 5.1.0(esbuild@0.27.2) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 debug: 4.4.1 - esbuild: 0.27.0 + esbuild: 0.27.2 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1