From 9bc3a34863bae0158f1da7ff69b9bcb64a60a632 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 16:47:11 +0100 Subject: [PATCH 01/18] Remove dependency utility for hardcoded widgets --- packages/command-tests/commands.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/packages/command-tests/commands.js b/packages/command-tests/commands.js index ad098ab7..4d3994b9 100644 --- a/packages/command-tests/commands.js +++ b/packages/command-tests/commands.js @@ -193,9 +193,6 @@ async function main() { widgetPackageJson = await readJson(join(workDir, "package.json")); widgetPackageJson.devDependencies["@mendix/pluggable-widgets-tools"] = toolsPackagePath; - // Adds compatibility to new React 18 and React native 0.72 - fixPackageJson(widgetPackageJson); - await writeJson(join(workDir, "package.json"), widgetPackageJson); await execAsync("npm install --loglevel=error", workDir, logger); @@ -400,23 +397,3 @@ async function execFailedAsync(command, workDir) { throw new Error(`Expected '${command}' to fail, but it didn't!`); } -function fixPackageJson(json) { - const devDependencies = { - "@types/jest": "^29.0.0", - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0", - "@types/react-test-renderer": "~18.0.0" - }; - const overrides = { - react: "^19.3.0", - "react-dom": "^19.3.0", - "react-native": "0.88.0" - }; - - Object.keys(devDependencies) - .filter(dep => !!json.devDependencies[dep]) - .forEach(dep => (json.devDependencies[dep] = devDependencies[dep])); - - json.overrides = overrides; - json.resolutions = overrides; -} From 50a6aaa54860bb6108cdd228553c34cffd95186f Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Thu, 23 Jul 2026 17:20:11 +0200 Subject: [PATCH 02/18] Use a shorter name for widgets --- packages/command-tests/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/command-tests/commands.js b/packages/command-tests/commands.js index 4d3994b9..a403757d 100644 --- a/packages/command-tests/commands.js +++ b/packages/command-tests/commands.js @@ -353,7 +353,7 @@ async function main() { } function getWidgetName(platform, boilerplate, lang, version) { - return `[generated_${version.replace(".", "_")}_${lang}_${platform}_${boilerplate}]`; + return `[${version.replace(".", "_")}_${lang}_${platform}_${boilerplate}]`; } function getWidgetLogger(index, platform, boilerplate, lang, version) { From 46e264af5271fbb64d3af736583ddaca0da04b8f Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Thu, 23 Jul 2026 17:22:14 +0200 Subject: [PATCH 03/18] Print error inline with other widget output --- packages/command-tests/commands.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/command-tests/commands.js b/packages/command-tests/commands.js index a403757d..2e0ba745 100644 --- a/packages/command-tests/commands.js +++ b/packages/command-tests/commands.js @@ -84,6 +84,10 @@ async function main() { return undefined; } catch (e) { logger(chalk.bold.red("Stopped with error")); + e.toString() + .split("\n") + .forEach(l => logger(chalk.red(l))); + logger(chalk.bold.red(`Widget Directory ${workDir}`)); return [config, e]; } finally { workDirs.push(workDir); @@ -102,11 +106,6 @@ async function main() { console.log("All done! Failed: %d Successful: %d", failures.length, CONFIGS.length - failures.length); - if (failures.length) { - failures.forEach(f => console.error(chalk.red(`Test for configuration ${f[0]} failed: ${f[1]}`))); - process.exit(2); - } - async function runTest(workDir, logger, platform, boilerplate, lang, version) { const isNative = platform === "native"; const widgetName = getWidgetName(platform, boilerplate, lang, version); From 121e8abed4c095d3ade315a6c819c871c3e67adf Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Thu, 23 Jul 2026 17:22:49 +0200 Subject: [PATCH 04/18] Allow interactive users to wait with cleaning up temporary files --- packages/command-tests/commands.js | 80 ++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/packages/command-tests/commands.js b/packages/command-tests/commands.js index 2e0ba745..d376bcef 100644 --- a/packages/command-tests/commands.js +++ b/packages/command-tests/commands.js @@ -9,6 +9,7 @@ import kill from "tree-kill"; import { promisify } from "util"; import chalk from "chalk"; import helpers from "yeoman-test"; +import { createInterface } from "readline/promises"; const { copy, existsSync, readJson, writeJson } = fsExtra; const { ls, mkdir, rm, tempdir } = shelljs; @@ -46,12 +47,17 @@ if (LIMIT_TESTS) { CONFIGS.splice(1, CONFIGS.length - 2); // Remove all configs except the first and the last } +const readline = createInterface(process.stdin, process.stdout); const yeomanMutex = new Mutex(); -main().catch(e => { - console.error(e); - process.exit(1); -}); +main() + .catch(e => { + console.error(e); + process.exitCode = 1; + }) + .finally(() => { + readline.close(); + }); async function main() { console.log("Preparing..."); @@ -63,6 +69,7 @@ async function main() { const toolsPackagePath = join(pluggableWidgetsToolsPath, packOutput.trim().split(/\n/g).pop()); const workDirs = []; + const configWorkDirs = {}; const workDirSemaphore = new Semaphore(PARALLELISM); const failures = ( await Promise.all( @@ -80,31 +87,64 @@ async function main() { ); mkdir("-p", workDir); } + configWorkDirs[workDir] = config; await runTest(workDir, logger, ...config); + workDirs.push(workDir); return undefined; - } catch (e) { + } catch (error) { logger(chalk.bold.red("Stopped with error")); - e.toString() + error + .toString() .split("\n") .forEach(l => logger(chalk.red(l))); logger(chalk.bold.red(`Widget Directory ${workDir}`)); - return [config, e]; + return { config, error, workDir }; } finally { - workDirs.push(workDir); release(); } }) ) ).filter(f => f); - console.log("Cleaning up temporary files"); - try { - rm("-rf", toolsPackagePath, ...workDirs); - } catch (error) { - console.warn(chalk.yellow(`Unable to remove temporary files: ${error.message}`)); + console.log( + "\nFinished Testing: " + + chalk.bold(chalk[failures.length > 0 ? "red" : "green"]("%d Failed, ") + chalk.green("%d Successful")), + failures.length, + CONFIGS.length - failures.length + ); + + console.log(chalk.green("\nCreated %d temporary directories for test widgets"), workDirs.length); + let maxLength = 0; + Object.entries(configWorkDirs) + .map(([dir, config]) => { + const name = getWidgetName(...config); + maxLength = Math.max(maxLength, name.length); + return [dir, name]; + }) + .forEach(([dir, name]) => + console.log( + " %s\t%s\t%s", + name + repeat(" ", maxLength - name.length), + dir, + failures.some(f => f.workDir === dir) ? chalk.bold.red("❌ Error") : "" + ) + ); + + if ( + !readline.terminal || // If non-interactive, just clean up without asking. + /^y?e?s?$/i.test(await readline.question(chalk.cyan("Clean up test widgets? ") + chalk.gray("(YES/no)"))) + ) { + console.log("Cleaning up temporary files"); + try { + rm("-rf", toolsPackagePath, ...workDirs); + } catch (error) { + console.warn(chalk.yellow(`Unable to remove temporary files: ${error.message}`)); + } + } else { + console.log("Leaving temporary files"); } - console.log("All done! Failed: %d Successful: %d", failures.length, CONFIGS.length - failures.length); + console.log(chalk.bold.green("\nAll done!")); async function runTest(workDir, logger, platform, boilerplate, lang, version) { const isNative = platform === "native"; @@ -258,8 +298,8 @@ async function main() { ? "@mendix/pluggable-widgets-tools" : null : boilerplate === "full" - ? "classnames" - : null; + ? "classnames" + : null; if ( packageName && @@ -396,3 +436,11 @@ async function execFailedAsync(command, workDir) { throw new Error(`Expected '${command}' to fail, but it didn't!`); } +function repeat(element, amount) { + let result = element; + while (amount > 0) { + result += element; + amount--; + } + return result; +} From d9fc177059c40fd98684cf23f71b4fbcba94ba31 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Mon, 27 Jul 2026 14:51:04 +0200 Subject: [PATCH 05/18] Migrate to Typescript and split into modules --- packages/command-tests/commands.js | 446 ------------------- packages/command-tests/lib/config.ts | 10 + packages/command-tests/lib/errors.ts | 9 + packages/command-tests/lib/exec.ts | 26 ++ packages/command-tests/lib/logger.ts | 31 ++ packages/command-tests/lib/node.ts | 14 + packages/command-tests/lib/option.ts | 12 + packages/command-tests/lib/strings.ts | 13 + packages/command-tests/main.ts | 155 +++++++ packages/command-tests/package.json | 26 +- packages/command-tests/tests/testRunner.ts | 283 ++++++++++++ packages/command-tests/tsconfig.json | 9 + pnpm-lock.yaml | 491 +++++++++++---------- 13 files changed, 829 insertions(+), 696 deletions(-) delete mode 100644 packages/command-tests/commands.js create mode 100644 packages/command-tests/lib/config.ts create mode 100644 packages/command-tests/lib/errors.ts create mode 100644 packages/command-tests/lib/exec.ts create mode 100644 packages/command-tests/lib/logger.ts create mode 100644 packages/command-tests/lib/node.ts create mode 100644 packages/command-tests/lib/option.ts create mode 100644 packages/command-tests/lib/strings.ts create mode 100644 packages/command-tests/main.ts create mode 100644 packages/command-tests/tests/testRunner.ts create mode 100644 packages/command-tests/tsconfig.json diff --git a/packages/command-tests/commands.js b/packages/command-tests/commands.js deleted file mode 100644 index d376bcef..00000000 --- a/packages/command-tests/commands.js +++ /dev/null @@ -1,446 +0,0 @@ -import { Mutex, Semaphore } from "async-mutex"; -import { exec } from "child_process"; -import { readFileSync, writeFileSync } from "fs"; -import fsExtra from "fs-extra"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; -import shelljs from "shelljs"; -import kill from "tree-kill"; -import { promisify } from "util"; -import chalk from "chalk"; -import helpers from "yeoman-test"; -import { createInterface } from "readline/promises"; - -const { copy, existsSync, readJson, writeJson } = fsExtra; -const { ls, mkdir, rm, tempdir } = shelljs; - -const LIMIT_TESTS = !!process.env.LIMIT_TESTS; -const PARALLELISM = 4; - -const DIR_COMMAND_TESTS = dirname(fileURLToPath(import.meta.url)); - -const CONFIGS = [ - ["web", "full", "js", "latest"], - ["web", "full", "ts", "latest"], - ["native", "full", "js", "latest"], - ["native", "full", "ts", "latest"], - ["web", "empty", "js", "latest"], - ["web", "empty", "ts", "latest"], - ["native", "empty", "js", "latest"], - ["native", "empty", "ts", "latest"] -]; - -const COLORS = [ - "green", - "yellow", - "blue", - "magenta", - "cyan", - "greenBright", - "yellowBright", - "blueBright", - "magentaBright", - "cyanBright" -]; - -if (LIMIT_TESTS) { - CONFIGS.splice(1, CONFIGS.length - 2); // Remove all configs except the first and the last -} - -const readline = createInterface(process.stdin, process.stdout); -const yeomanMutex = new Mutex(); - -main() - .catch(e => { - console.error(e); - process.exitCode = 1; - }) - .finally(() => { - readline.close(); - }); - -async function main() { - console.log("Preparing..."); - - const pluggableWidgetsToolsPath = join(DIR_COMMAND_TESTS, "../pluggable-widgets-tools"); - const pluggableWidgetsToolsVersion = (await readJson(join(pluggableWidgetsToolsPath, "package.json"))).version; - console.log("Preparing: Packaging @mendix/pluggable-widgets-tools version %s", pluggableWidgetsToolsVersion); - const { stdout: packOutput } = await execAsync("npm pack", pluggableWidgetsToolsPath, m => console.log(m)); - const toolsPackagePath = join(pluggableWidgetsToolsPath, packOutput.trim().split(/\n/g).pop()); - - const workDirs = []; - const configWorkDirs = {}; - const workDirSemaphore = new Semaphore(PARALLELISM); - const failures = ( - await Promise.all( - CONFIGS.map(async (config, index) => { - const logger = getWidgetLogger(index, ...config); - logger("Scheduled, waiting for lock"); - const [, release] = await workDirSemaphore.acquire(); - let workDir; - try { - workDir = workDirs.pop(); - if (!workDir) { - workDir = join( - index === 0 ? join(tempdir(), "spaced folder") : tempdir(), - `pwt_test_${Math.round(Math.random() * 10000)}` - ); - mkdir("-p", workDir); - } - configWorkDirs[workDir] = config; - await runTest(workDir, logger, ...config); - workDirs.push(workDir); - return undefined; - } catch (error) { - logger(chalk.bold.red("Stopped with error")); - error - .toString() - .split("\n") - .forEach(l => logger(chalk.red(l))); - logger(chalk.bold.red(`Widget Directory ${workDir}`)); - return { config, error, workDir }; - } finally { - release(); - } - }) - ) - ).filter(f => f); - - console.log( - "\nFinished Testing: " + - chalk.bold(chalk[failures.length > 0 ? "red" : "green"]("%d Failed, ") + chalk.green("%d Successful")), - failures.length, - CONFIGS.length - failures.length - ); - - console.log(chalk.green("\nCreated %d temporary directories for test widgets"), workDirs.length); - let maxLength = 0; - Object.entries(configWorkDirs) - .map(([dir, config]) => { - const name = getWidgetName(...config); - maxLength = Math.max(maxLength, name.length); - return [dir, name]; - }) - .forEach(([dir, name]) => - console.log( - " %s\t%s\t%s", - name + repeat(" ", maxLength - name.length), - dir, - failures.some(f => f.workDir === dir) ? chalk.bold.red("❌ Error") : "" - ) - ); - - if ( - !readline.terminal || // If non-interactive, just clean up without asking. - /^y?e?s?$/i.test(await readline.question(chalk.cyan("Clean up test widgets? ") + chalk.gray("(YES/no)"))) - ) { - console.log("Cleaning up temporary files"); - try { - rm("-rf", toolsPackagePath, ...workDirs); - } catch (error) { - console.warn(chalk.yellow(`Unable to remove temporary files: ${error.message}`)); - } - } else { - console.log("Leaving temporary files"); - } - - console.log(chalk.bold.green("\nAll done!")); - - async function runTest(workDir, logger, platform, boilerplate, lang, version) { - const isNative = platform === "native"; - const widgetName = getWidgetName(platform, boilerplate, lang, version); - let widgetPackageJson; - - logger(`Preparing widget...`); - await prepareWidget(); - logger(`Ready to test!`); - - logger(`Testing linting...`); - await testLint(); - - // Temporarily disabled due to bizarre typing issues in the CI that cannot be reproduced in any local environment - logger(`Testing unit tests....`); - await testTest(); - - if (LIMIT_TESTS) { - logger(`Quick tested!`); - return; - } - - logger(`Testing 'build' command...`); - await testBuild(); - - logger(`Testing 'release' command...`); - await testRelease(); - - logger(`Checking dependencies files...`); - await checkDependenciesFiles(isNative, boilerplate, version); - - logger(`Testing npm start...`); - await testStart(); - - // Check native dependency management - if (isNative) { - logger(`Testing native dependency management...`); - await testNativeDependencyManagement(); - } - - logger(`Tested!`); - - async function prepareWidget() { - const filesToRemove = ls(workDir) - .filter(file => file !== "node_modules") - .map(file => join(workDir, file)); - - if (filesToRemove.length) { - rm("-r", ...filesToRemove); - } - - if (version === "latest") { - logger("Preparing widget: Generating"); - const promptAnswers = { - name: "Generated", - description: "My widget description", - organization: "com.mendix", - copyright: "Mendix 2020", - license: "Apache-2.0", - version: "1.0.0", - author: "Widget Generator", - projectPath: "./dist/MxTestProject", - programmingLanguage: lang === "ts" ? "typescript" : "javascript", - platform, - boilerplate, - hasUnitTests: true, - hasE2eTests: false - }; - const release = await yeomanMutex.acquire(); // yeoman generator is no re-entrable :( - try { - const generatedWidget = await helpers - .run(resolveModule("@mendix/generator-widget")) - .withAnswers(promptAnswers) - .withArguments("Generated") - .toPromise(); - await copy(join(generatedWidget.cwd, "generated"), workDir); - } finally { - release(); - } - } else { - await copy(join(DIR_COMMAND_TESTS, "projects", widgetName), workDir); - } - - logger("Preparing widget: Installing"); - widgetPackageJson = await readJson(join(workDir, "package.json")); - widgetPackageJson.devDependencies["@mendix/pluggable-widgets-tools"] = toolsPackagePath; - - await writeJson(join(workDir, "package.json"), widgetPackageJson); - - await execAsync("npm install --loglevel=error", workDir, logger); - } - - async function testLint() { - await execFailedAsync("npm run lint", workDir); - await execAsync("npm run lint:fix", workDir, logger); - await execAsync("npm run lint", workDir, logger); - } - - async function testTest() { - if (platform === "native") { - await execFailedAsync("npm test -- --forceExit", workDir); - await execAsync("npm test -- -u --forceExit", workDir, logger); - } else { - await execAsync("npm test -- --forceExit", workDir, logger); - } - } - - async function testBuild() { - await execAsync("npm run build", workDir, logger); - checkWidgetBundleFiles(); - } - - async function testRelease() { - rm("-rf", join(workDir, "dist")); - await execAsync("npm run release", workDir, logger); - checkWidgetBundleFiles(); - } - - function checkWidgetBundleFiles() { - const stagingDir = join(workDir, "dist", "tmp", "widgets"); - const mpkFile = join( - workDir, - "dist", - widgetPackageJson.version, - `${widgetPackageJson.packagePath}.${widgetPackageJson.widgetName}.mpk` - ); - const requiredFiles = [ - mpkFile, - join(stagingDir, "package.xml"), - join(stagingDir, `${widgetPackageJson.widgetName}.xml`) - ]; - const missing = requiredFiles.filter(f => !existsSync(f)); - if (missing.length) { - throw new Error(`Expected widget bundle files in mpk, but missing: ${missing.join(", ")}.`); - } - } - - async function checkDependenciesFiles(isNative, boilerplate, version) { - const dependenciesJSONFile = join(workDir, "dist", "tmp", "widgets", "dependencies.json"); - const dependenciesTxtFile = join(workDir, "dist", "tmp", "widgets", "dependencies.txt"); - - if (!existsSync(dependenciesJSONFile) || !existsSync(dependenciesTxtFile)) { - throw new Error("Expected dependencies files to be generated, but it wasn't."); - } - - const dependenciesJSON = JSON.parse(readFileSync(dependenciesJSONFile, "utf8")); - const dependenciesText = readFileSync(dependenciesTxtFile, "utf8"); - - const packageName = isNative - ? version === "latest" - ? "@mendix/pluggable-widgets-tools" - : null - : boilerplate === "full" - ? "classnames" - : null; - - if ( - packageName && - (!dependenciesJSON.some(dependency => Object.keys(dependency)[0].includes(packageName)) || - !dependenciesText.includes(packageName)) - ) { - throw new Error(`The "${packageName}" could not be found in the dependencies files.`); - } else if ( - !packageName && - (dependenciesJSON.length !== 0 || - !(dependenciesText.includes("No third parties dependencies") || dependenciesText === "")) - ) { - throw new Error("Unexpected content in dependencies files."); - } - } - - async function testStart() { - const startProcess = exec("npm start", { cwd: workDir, env: { ...process.env, NO_COLOR: "true" } }); - - try { - await new Promise((resolve, reject) => { - let inProgress = false; - startProcess.stdout.on("data", onOutput); - startProcess.stderr.on("data", onOutput); - startProcess.on("exit", exitCode => { - reject(new Error(`Exited with status ${exitCode}`)); - }); - function onOutput(data) { - if (/error/i.test(data)) { - reject(new Error(`Received error ${data}`)); - } else if (/\bbundles /.test(data)) { - inProgress = true; - } else if (/\bcreated .* in /.test(data)) { - inProgress = false; - setTimeout(() => { - if (!inProgress) { - logger(`Start succeeded!`); - resolve(); - } - }, 100); - } - } - }); - } finally { - try { - await promisify(kill)(startProcess.pid, "SIGKILL"); - } catch (e) { - console.warn(`[${widgetName}] Error while killing start process: ${e}`); - } - await new Promise(resolve => setTimeout(resolve, 5000)); // give time for processes to die - } - } - - async function testNativeDependencyManagement() { - const NATIVE_MAPS_VERSION = "0.31.1"; - await execAsync(`npm install react-native-maps@${NATIVE_MAPS_VERSION}`, workDir, logger); - const entryPointPath = join(workDir, "src", `Generated.${lang}x`); - const jsonPath = join(workDir, `/dist/tmp/widgets/${widgetPackageJson.widgetName}.json`); - const fileData = readFileSync(entryPointPath); - writeFileSync( - entryPointPath, - Buffer.concat([ - Buffer.from(`import "react-native-maps"; -`), - Buffer.from(fileData) - ]) - ); - await execAsync("npm run build", workDir, logger); - if (!existsSync(jsonPath)) { - throw new Error("Expected dependency json file to be generated, but it wasn't."); - } - const dependencyJson = await readJson(jsonPath); - if ( - !dependencyJson.nativeDependencies || - dependencyJson.nativeDependencies["react-native-maps"] !== NATIVE_MAPS_VERSION - ) { - throw new Error("Expected dependency json file to contain dependencies, but it wasn't."); - } - if (!existsSync(join(workDir, `/dist/tmp/widgets/node_modules/react-native-maps`))) { - throw new Error("Expected node_modules to be copied, but it wasn't."); - } - if ( - !existsSync(join(workDir, `/dist/tmp/widgets/node_modules/react-native-maps/node_modules/prop-types`)) - ) { - throw new Error("Expected transitive node_modules to be copied, but it wasn't."); - } - logger(`Native dependency management succeeded!`); - } - } -} - -function getWidgetName(platform, boilerplate, lang, version) { - return `[${version.replace(".", "_")}_${lang}_${platform}_${boilerplate}]`; -} - -function getWidgetLogger(index, platform, boilerplate, lang, version) { - const color = chalk[COLORS[index % COLORS.length]]; - return (...msgs) => console.log(color(getWidgetName(platform, boilerplate, lang, version)), ...msgs); -} - -/** - * Resolves the given module and returns the full path. - * This corresponds to the "main" or "import" property of a package.json. - * - * @example - * ```js - * resolve("@mendix/generator-widget") // "/path/to/generator-widget/generators/app/index.js" - * ``` - */ -function resolveModule(packageName) { - return fileURLToPath(import.meta.resolve(packageName)); -} - -async function execAsync(command, workDir, logger) { - const resultPromise = promisify(exec)(command, { cwd: workDir }); - while (true) { - const waitPromise = new Promise(resolve => setTimeout(resolve, 60 * 1000)); - - const haveCompleted = await Promise.race([resultPromise.then(() => true), waitPromise.then(() => false)]); - if (haveCompleted) { - return resultPromise; - } - logger("Waiting..."); - } -} - -async function execFailedAsync(command, workDir) { - try { - await promisify(exec)(command, { cwd: workDir }); - // eslint-disable-next-line no-unused-vars - } catch (_) { - return; - } - throw new Error(`Expected '${command}' to fail, but it didn't!`); -} - -function repeat(element, amount) { - let result = element; - while (amount > 0) { - result += element; - amount--; - } - return result; -} diff --git a/packages/command-tests/lib/config.ts b/packages/command-tests/lib/config.ts new file mode 100644 index 00000000..aaecb17d --- /dev/null +++ b/packages/command-tests/lib/config.ts @@ -0,0 +1,10 @@ +export type Config = [ + platform: "web" | "native", + boilerplate: "full" | "empty", + language: "ts" | "js", + version: "latest" +]; + +export function getWidgetName(...[platform, boilerplate, lang, version]: Config): string { + return `[${version.replace(".", "_")}_${lang}_${platform}_${boilerplate}]`; +} diff --git a/packages/command-tests/lib/errors.ts b/packages/command-tests/lib/errors.ts new file mode 100644 index 00000000..b83f3cee --- /dev/null +++ b/packages/command-tests/lib/errors.ts @@ -0,0 +1,9 @@ +export function ensureError(input: unknown): Error { + if (input instanceof Error) { + return input; + } + if (typeof input === "string") { + return new Error(input); + } + return new Error(`Unknown Error: ${input}`); +} diff --git a/packages/command-tests/lib/exec.ts b/packages/command-tests/lib/exec.ts new file mode 100644 index 00000000..0490617b --- /dev/null +++ b/packages/command-tests/lib/exec.ts @@ -0,0 +1,26 @@ +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { type Logger } from "./logger.ts"; + +export async function execAsync(command: string, workDir: string, logger: Logger) { + const resultPromise = promisify(exec)(command, { cwd: workDir }); + while (true) { + const waitPromise = new Promise(resolve => setTimeout(resolve, 60 * 1000)); + + const haveCompleted = await Promise.race([resultPromise.then(() => true), waitPromise.then(() => false)]); + if (haveCompleted) { + return resultPromise; + } + logger("Waiting..."); + } +} + +export async function execFailedAsync(command: string, workDir: string) { + try { + await promisify(exec)(command, { cwd: workDir }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return; + } + throw new Error(`Expected '${command}' to fail, but it didn't!`); +} diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts new file mode 100644 index 00000000..737c2cd6 --- /dev/null +++ b/packages/command-tests/lib/logger.ts @@ -0,0 +1,31 @@ +import { format, styleText } from "node:util"; +import { type Config, getWidgetName } from "./config.ts"; + +type Arguments = F extends (...args: infer Args) => unknown ? Args : never; +type TextStyles = Arguments[0]; + +const COLORS: TextStyles[] = [ + "green", + "yellow", + "blue", + "magenta", + "cyan", + "bgGreen", + "bgYellow", + "bgBlue", + "bgMagenta", + "bgCyan" +]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Logger = (template: string, ...msgs: any[]) => void; + +export function getWidgetLogger(index: number, ...[platform, boilerplate, lang, version]: Config): Logger { + const colorName = COLORS[index % COLORS.length]; + return (template, ...msgs) => + console.log( + "%s %s", + styleText(colorName, getWidgetName(platform, boilerplate, lang, version)), + format(template, ...msgs) + ); +} diff --git a/packages/command-tests/lib/node.ts b/packages/command-tests/lib/node.ts new file mode 100644 index 00000000..70242303 --- /dev/null +++ b/packages/command-tests/lib/node.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from "node:url"; + +/** + * Resolves the given module and returns the full path. + * This corresponds to the "main" or "import" property of a package.json. + * + * @example + * ```js + * resolve("@mendix/generator-widget") // "/path/to/generator-widget/generators/app/index.js" + * ``` + */ +export function resolveModule(packageName: string): string { + return fileURLToPath(import.meta.resolve(packageName)); +} diff --git a/packages/command-tests/lib/option.ts b/packages/command-tests/lib/option.ts new file mode 100644 index 00000000..36ce2dcb --- /dev/null +++ b/packages/command-tests/lib/option.ts @@ -0,0 +1,12 @@ +export type Option = T | undefined; + +export function ensure(input: T | undefined): T { + if (isDefined(input)) { + return input; + } + throw Error("Unexpected value of undefined"); +} + +export function isDefined(input: T | undefined): input is T { + return input !== undefined; +} diff --git a/packages/command-tests/lib/strings.ts b/packages/command-tests/lib/strings.ts new file mode 100644 index 00000000..3f2fa00b --- /dev/null +++ b/packages/command-tests/lib/strings.ts @@ -0,0 +1,13 @@ +/*** + * @param element The element to repeat + * @param amount The amount of times to repeat the element + * @returns String containing the element amount times. + */ +export function repeat(element: string, amount: number) { + let result = element; + while (amount > 0) { + result += element; + amount--; + } + return result; +} diff --git a/packages/command-tests/main.ts b/packages/command-tests/main.ts new file mode 100644 index 00000000..8969ae23 --- /dev/null +++ b/packages/command-tests/main.ts @@ -0,0 +1,155 @@ +#! /usr/bin/env node --experimental-strip-types +import { Mutex, Semaphore } from "async-mutex"; +import fsExtra from "fs-extra"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import shelljs from "shelljs"; +import { createInterface } from "readline/promises"; +import { execAsync } from "./lib/exec.ts"; +import { ensure, isDefined } from "./lib/option.ts"; +import { getWidgetLogger } from "./lib/logger.ts"; +import { type Config, getWidgetName } from "./lib/config.ts"; +import { repeat } from "./lib/strings.ts"; +import { runTest } from "./tests/testRunner.ts"; +import { ensureError } from "./lib/errors.ts"; +import { styleText } from "node:util"; + +const { readJson } = fsExtra; +const { mkdir, rm, tempdir } = shelljs; + +const LIMIT_TESTS = !!process.env.LIMIT_TESTS; +const PARALLELISM = 4; + +const DIR_COMMAND_TESTS = dirname(fileURLToPath(import.meta.url)); + +const CONFIGS: Config[] = [ + ["web", "full", "js", "latest"], + ["web", "full", "ts", "latest"], + ["native", "full", "js", "latest"], + ["native", "full", "ts", "latest"], + ["web", "empty", "js", "latest"], + ["web", "empty", "ts", "latest"], + ["native", "empty", "js", "latest"], + ["native", "empty", "ts", "latest"] +]; + +if (LIMIT_TESTS) { + CONFIGS.splice(1, CONFIGS.length - 2); // Remove all configs except the first and the last +} + +const readline = createInterface(process.stdin, process.stdout); +const yeomanMutex = new Mutex(); + +main() + .catch(e => { + console.error(e); + process.exitCode = 1; + }) + .finally(() => { + readline.close(); + }); + +async function main() { + console.log("Preparing..."); + + const pluggableWidgetsToolsPath = join(DIR_COMMAND_TESTS, "../pluggable-widgets-tools"); + const pluggableWidgetsToolsVersion = (await readJson(join(pluggableWidgetsToolsPath, "package.json"))).version; + console.log("Preparing: Packaging @mendix/pluggable-widgets-tools version %s", pluggableWidgetsToolsVersion); + const { stdout: packOutput } = await execAsync("npm pack", pluggableWidgetsToolsPath, (m: string) => + console.log(m) + ); + const widgetsToolsPackagePath = join(pluggableWidgetsToolsPath, ensure(packOutput.trim().split(/\n/g).pop())); + + const workDirs: string[] = []; + const configWorkDirs: Record = {}; + const workDirSemaphore = new Semaphore(PARALLELISM); + const failures: Array<{ config: Config; error: Error; workDir: string }> = ( + await Promise.all( + CONFIGS.map(async (config, index) => { + const logger = getWidgetLogger(index, ...config); + logger("Scheduled, waiting for lock"); + const [, release] = await workDirSemaphore.acquire(); + let workDir; + try { + workDir = workDirs.pop(); + if (!workDir) { + workDir = join( + index === 0 ? join(tempdir(), "spaced folder") : tempdir(), + `pwt_test_${Math.round(Math.random() * 10000)}` + ); + mkdir("-p", workDir); + } + configWorkDirs[workDir] = config; + await runTest(config, { + logger, + yeomanMutex, + workDir, + widgetsToolsPackagePath, + isShortRun: LIMIT_TESTS, + commandTestsDir: DIR_COMMAND_TESTS + }); + workDirs.push(workDir); + return undefined; + } catch (e) { + logger(styleText(["bold", "red"], "Stopped with error")); + const error = ensureError(e); + error + .toString() + .split("\n") + .forEach(l => logger(styleText("red", l))); + logger(styleText(["bold", "red"], `Widget Directory ${workDir}`)); + return { config, error, workDir: workDir ?? "" }; + } finally { + release(); + } + }) + ) + ).filter(isDefined); + + console.log( + "\nFinished Testing: " + + styleText( + "bold", + styleText(failures.length > 0 ? "red" : "green", "%d Failed, ") + styleText("green", "%d Successful") + ), + failures.length, + CONFIGS.length - failures.length + ); + + console.log(styleText("green", "\nCreated %d temporary directories during testing"), workDirs.length); + let maxDirLength = 0, + maxNameLength = 0; + Object.entries(configWorkDirs) + .map(([dir, config]) => { + const name = getWidgetName(...config); + maxDirLength = Math.max(maxDirLength, dir.length); + maxNameLength = Math.max(maxNameLength, name.length); + return [dir, name]; + }) + .forEach(([dir, name]) => + console.log( + " %s %s %s", + dir + repeat(" ", maxDirLength - dir.length), + name + repeat(" ", maxNameLength - name.length), + failures.some(f => f.workDir === dir) ? styleText(["bold", "red"], "❌ Error") : "" + ) + ); + + if ( + !readline.terminal || // If non-interactive, just clean up without asking. + /^y?e?s?$/i.test( + await readline.question(styleText("cyan", "Clean up test widgets? ") + styleText("gray", "(YES/no)")) + ) + ) { + console.log("Cleaning up temporary files"); + try { + rm("-rf", widgetsToolsPackagePath, ...workDirs); + } catch (error) { + console.warn(styleText("yellow", `Unable to remove temporary files: ${ensureError(error).message}`)); + } + } else { + console.log("Leaving temporary files"); + } + + console.log(styleText(["bold", "green"], "\nAll done!")); +} diff --git a/packages/command-tests/package.json b/packages/command-tests/package.json index 3dd153bb..8fde9a6f 100644 --- a/packages/command-tests/package.json +++ b/packages/command-tests/package.json @@ -4,11 +4,15 @@ "description": "Cli tests for widgets generator and tools", "private": true, "type": "module", - "main": "commands.js", + "main": "main.ts", "scripts": { - "start": "node commands.js" + "start": "node --experimental-strip-types main.ts", + "test": "tsc --noEmit" }, - "devDependencies": { + "engines": { + "node": "^22.18.0" + }, + "dependencies": { "@mendix/generator-widget": "workspace:*", "@mendix/pluggable-widgets-tools": "workspace:*", "@prettier/plugin-xml": "^2.2.0", @@ -17,20 +21,20 @@ "@types/shelljs": "^0.10.0", "@types/xml2js": "^0.4.5", "async-mutex": "^0.5.0", - "chalk": "^5.6.2", - "eslint-config-prettier": "^8.5.0", "fs-extra": "^11.3.4", - "globals": "^17.6.0", - "identity-obj-proxy": "^3.0.0", - "peggy": "^1.2.0", "shelljs": "^0.10.0", - "shx": "^0.4.0", "tree-kill": "^1.2.2", - "ts-node": "^10.9.1", - "typescript": "^4.8.4", "yeoman-environment": "^6.0.1", "yeoman-test": "^11.3.1" }, + "devDependencies": { + "@tsconfig/node-ts": "^23.6.4", + "@tsconfig/node22": "^22.0.5", + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.14.8", + "@types/shelljs": "^0.10.0", + "typescript": "^5.9.3" + }, "keywords": [], "author": "", "license": "ISC" diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts new file mode 100644 index 00000000..55a94f23 --- /dev/null +++ b/packages/command-tests/tests/testRunner.ts @@ -0,0 +1,283 @@ +import shelljs from "shelljs"; +import kill from "tree-kill"; +import { type Config, getWidgetName } from "../lib/config.ts"; +import { type Logger } from "../lib/logger.ts"; +import { join } from "node:path"; +import { resolveModule } from "../lib/node.ts"; +import fsExtra from "fs-extra"; +import { execAsync, execFailedAsync } from "../lib/exec.ts"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import { Mutex } from "async-mutex"; +import helpers from "yeoman-test"; + +const { copy, existsSync, readFileSync, readJson, writeFileSync, writeJson } = fsExtra; +const { ls, rm } = shelljs; + +export async function runTest( + config: Config, + { + logger, + yeomanMutex, + workDir, + commandTestsDir, + widgetsToolsPackagePath, + isShortRun + }: { + logger: Logger; + yeomanMutex: Mutex; + workDir: string; + isShortRun: boolean; + commandTestsDir: string; + widgetsToolsPackagePath: string; + } +) { + const [platform, boilerplate, lang, version] = config; + const isNative = platform === "native"; + const widgetName = getWidgetName(platform, boilerplate, lang, version); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let widgetPackageJson: any; + + logger(`Preparing widget...`); + await prepareWidget(); + logger(`Ready to test!`); + + logger(`Testing linting...`); + await testLint(); + + // Temporarily disabled due to bizarre typing issues in the CI that cannot be reproduced in any local environment + logger(`Testing unit tests....`); + await testTest(); + + if (isShortRun) { + logger(`Quick tested!`); + return; + } + + logger(`Testing 'build' command...`); + await testBuild(); + + logger(`Testing 'release' command...`); + await testRelease(); + + logger(`Checking dependencies files...`); + await checkDependenciesFiles(isNative, boilerplate, version); + + logger(`Testing npm start...`); + await testStart(); + + // Check native dependency management + if (isNative) { + logger(`Testing native dependency management...`); + await testNativeDependencyManagement(); + } + + logger(`Tested!`); + + async function prepareWidget() { + const filesToRemove = ls(workDir) + .filter(file => file !== "node_modules") + .map(file => join(workDir, file)); + + if (filesToRemove.length) { + rm("-r", ...filesToRemove); + } + + if (version === "latest") { + logger("Preparing widget: Generating"); + const promptAnswers = { + name: "Generated", + description: "My widget description", + organization: "com.mendix", + copyright: "Mendix 2020", + license: "Apache-2.0", + version: "1.0.0", + author: "Widget Generator", + projectPath: "./dist/MxTestProject", + programmingLanguage: lang === "ts" ? "typescript" : "javascript", + platform, + boilerplate, + hasUnitTests: true, + hasE2eTests: false + }; + const release = await yeomanMutex.acquire(); // yeoman generator is no re-entrable :( + try { + const generatedWidget = await helpers + .run(resolveModule("@mendix/generator-widget")) + .withAnswers(promptAnswers) + .withArguments("Generated") + .run(); + await copy(join(generatedWidget.cwd, "generated"), workDir); + } finally { + release(); + } + } else { + await copy(join(commandTestsDir, "projects", widgetName), workDir); + } + + logger("Preparing widget: Installing"); + widgetPackageJson = await readJson(join(workDir, "package.json")); + widgetPackageJson.devDependencies["@mendix/pluggable-widgets-tools"] = widgetsToolsPackagePath; + + await writeJson(join(workDir, "package.json"), widgetPackageJson); + + await execAsync("npm install --loglevel=error", workDir, logger); + } + + async function testLint() { + await execFailedAsync("npm run lint", workDir); + await execAsync("npm run lint:fix", workDir, logger); + await execAsync("npm run lint", workDir, logger); + } + + async function testTest() { + if (platform === "native") { + await execFailedAsync("npm test -- --forceExit", workDir); + await execAsync("npm test -- -u --forceExit", workDir, logger); + } else { + await execAsync("npm test -- --forceExit", workDir, logger); + } + } + + async function testBuild() { + await execAsync("npm run build", workDir, logger); + checkWidgetBundleFiles(); + } + + async function testRelease() { + rm("-rf", join(workDir, "dist")); + await execAsync("npm run release", workDir, logger); + checkWidgetBundleFiles(); + } + + function checkWidgetBundleFiles() { + const stagingDir = join(workDir, "dist", "tmp", "widgets"); + const mpkFile = join( + workDir, + "dist", + widgetPackageJson.version, + `${widgetPackageJson.packagePath}.${widgetPackageJson.widgetName}.mpk` + ); + const requiredFiles = [ + mpkFile, + join(stagingDir, "package.xml"), + join(stagingDir, `${widgetPackageJson.widgetName}.xml`) + ]; + const missing = requiredFiles.filter(f => !existsSync(f)); + if (missing.length) { + throw new Error(`Expected widget bundle files in mpk, but missing: ${missing.join(", ")}.`); + } + } + + async function checkDependenciesFiles(isNative: boolean, boilerplate: Config[1], version: Config[3]) { + const dependenciesJSONFile = join(workDir, "dist", "tmp", "widgets", "dependencies.json"); + const dependenciesTxtFile = join(workDir, "dist", "tmp", "widgets", "dependencies.txt"); + + if (!existsSync(dependenciesJSONFile) || !existsSync(dependenciesTxtFile)) { + throw new Error("Expected dependencies files to be generated, but it wasn't."); + } + + type Dependency = { [packageName: string]: { version: string; url: string | null } }; + const dependenciesJSON: Dependency[] = JSON.parse(readFileSync(dependenciesJSONFile, "utf8")); + const dependenciesText = readFileSync(dependenciesTxtFile, "utf8"); + + const packageName = isNative + ? version === "latest" + ? "@mendix/pluggable-widgets-tools" + : null + : boilerplate === "full" + ? "classnames" + : null; + + if ( + packageName && + (!dependenciesJSON.some(dependency => Object.keys(dependency).includes(packageName)) || + !dependenciesText.includes(packageName)) + ) { + throw new Error(`The "${packageName}" could not be found in the dependencies files.`); + } else if ( + !packageName && + (dependenciesJSON.length !== 0 || + !(dependenciesText.includes("No third parties dependencies") || dependenciesText === "")) + ) { + throw new Error("Unexpected content in dependencies files."); + } + } + + async function testStart() { + const startProcess = exec("npm start", { cwd: workDir, env: { ...process.env, NO_COLOR: "true" } }); + + try { + await new Promise((resolve, reject) => { + let inProgress = false; + startProcess.stdout?.on("data", onOutput); + startProcess.stderr?.on("data", onOutput); + startProcess.on("exit", exitCode => { + reject(new Error(`Exited with status ${exitCode}`)); + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function onOutput(data: any) { + if (/error/i.test(data)) { + reject(new Error(`Received error ${data}`)); + } else if (/\bbundles /.test(data)) { + inProgress = true; + } else if (/\bcreated .* in /.test(data)) { + inProgress = false; + setTimeout(() => { + if (!inProgress) { + logger(`Start succeeded!`); + resolve(); + } + }, 100); + } + } + }); + } finally { + if (startProcess.pid !== undefined) { + try { + await promisify((pid: number, signal: string, callback: (e?: Error) => void) => + kill(pid, signal, callback) + )(startProcess.pid, "SIGKILL"); + } catch (e) { + throw new Error("Unable to kill `start` process", { cause: e }); + } + logger("Waiting for process to terminate"); + await new Promise(resolve => setTimeout(resolve, 5000)); // give time for processes to die + } + } + } + + async function testNativeDependencyManagement() { + const NATIVE_MAPS_VERSION = "0.31.1"; + await execAsync(`npm install react-native-maps@${NATIVE_MAPS_VERSION}`, workDir, logger); + const entryPointPath = join(workDir, "src", `Generated.${lang}x`); + const jsonPath = join(workDir, `/dist/tmp/widgets/${widgetPackageJson.widgetName}.json`); + const fileData = readFileSync(entryPointPath); + writeFileSync( + entryPointPath, + Buffer.concat([ + Buffer.from(`import "react-native-maps"; +`), + Buffer.from(fileData) + ]) + ); + await execAsync("npm run build", workDir, logger); + if (!existsSync(jsonPath)) { + throw new Error("Expected dependency json file to be generated, but it wasn't."); + } + const dependencyJson = await readJson(jsonPath); + if ( + !dependencyJson.nativeDependencies || + dependencyJson.nativeDependencies["react-native-maps"] !== NATIVE_MAPS_VERSION + ) { + throw new Error("Expected dependency json file to contain dependencies, but it wasn't."); + } + if (!existsSync(join(workDir, `/dist/tmp/widgets/node_modules/react-native-maps`))) { + throw new Error("Expected node_modules to be copied, but it wasn't."); + } + if (!existsSync(join(workDir, `/dist/tmp/widgets/node_modules/react-native-maps/node_modules/prop-types`))) { + throw new Error("Expected transitive node_modules to be copied, but it wasn't."); + } + logger(`Native dependency management succeeded!`); + } +} diff --git a/packages/command-tests/tsconfig.json b/packages/command-tests/tsconfig.json new file mode 100644 index 00000000..8a5c7ecc --- /dev/null +++ b/packages/command-tests/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": [ + "@tsconfig/node22/tsconfig.json", + "@tsconfig/node-ts/tsconfig.json" + ], + "compilerOptions": { +"allowImportingTsExtensions": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4ef8835..97291f3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 8.59.3(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) packages/command-tests: - devDependencies: + dependencies: '@mendix/generator-widget': specifier: workspace:* version: link:../generator-widget @@ -71,45 +71,34 @@ importers: async-mutex: specifier: ^0.5.0 version: 0.5.0 - chalk: - specifier: ^5.6.2 - version: 5.6.2 - eslint-config-prettier: - specifier: ^8.5.0 - version: 8.10.2(eslint@10.4.0(jiti@2.7.0)) fs-extra: specifier: ^11.3.4 version: 11.3.5 - globals: - specifier: ^17.6.0 - version: 17.6.0 - identity-obj-proxy: - specifier: ^3.0.0 - version: 3.0.0 - peggy: - specifier: ^1.2.0 - version: 1.2.0 shelljs: specifier: ^0.10.0 version: 0.10.0 - shx: - specifier: ^0.4.0 - version: 0.4.0 tree-kill: specifier: ^1.2.2 version: 1.2.2 - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@4.9.5) - typescript: - specifier: ^4.8.4 - version: 4.9.5 yeoman-environment: specifier: ^6.0.1 version: 6.1.0(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4) yeoman-test: specifier: ^11.3.1 version: 11.5.2(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4)(yeoman-environment@6.1.0(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4))(yeoman-generator@8.2.2(@types/node@22.20.1)(@yeoman/types@1.11.1(@types/node@22.20.1)(@yeoman/adapter@4.0.2(@types/node@22.20.1))(mem-fs@4.1.4))(mem-fs@4.1.4)) + devDependencies: + '@tsconfig/node-ts': + specifier: ^23.6.4 + version: 23.6.4 + '@tsconfig/node22': + specifier: ^22.0.5 + version: 22.0.5 + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 + typescript: + specifier: ^5.9.3 + version: 5.9.3 packages/generator-widget: dependencies: @@ -167,7 +156,7 @@ importers: version: 0.88.0(@babel/core@7.29.0) '@react-native/jest-preset': specifier: 0.88.0 - version: 0.88.0(@babel/core@7.29.0)(react@19.3.0) + version: 0.88.0(@babel/core@7.29.0)(react@19.3.1) '@rollup/plugin-alias': specifier: ^5.1.1 version: 5.1.1(rollup@4.60.4) @@ -212,10 +201,10 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1) '@testing-library/react-native': specifier: ^13.3.3 - version: 13.3.3(jest@30.5.1(@types/node@22.20.1)(ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@5.9.3)))(react-native@0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0))(react-test-renderer@19.3.0(react@19.3.0))(react@19.3.0) + version: 13.3.3(jest@30.5.1(@types/node@22.20.1)(ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@5.9.3)))(react-native@0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1))(react-test-renderer@19.3.1(react@19.3.1))(react@19.3.1) '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) @@ -323,7 +312,7 @@ importers: version: 2.8.8 react-test-renderer: specifier: ^19.3.0 - version: 19.3.0(react@19.3.0) + version: 19.3.1(react@19.3.1) recursive-copy: specifier: ^2.0.11 version: 2.0.14 @@ -393,13 +382,13 @@ importers: version: 0.4.14 react: specifier: ^19.3.0 - version: 19.3.0 + version: 19.3.1 react-dom: specifier: ^19.3.0 - version: 19.3.0(react@19.3.0) + version: 19.3.1(react@19.3.1) react-native: specifier: ~0.88.0 - version: 0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0) + version: 0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1) packages/release-tools: dependencies: @@ -528,10 +517,18 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.11': + resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -544,6 +541,11 @@ packages: resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} + '@babel/parser@7.29.11': + resolution: {integrity: sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} @@ -1110,6 +1112,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.11': + resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -1936,8 +1942,8 @@ packages: peerDependencies: prettier: ^3.0.0 - '@react-native/asset-utils@0.88.0': - resolution: {integrity: sha512-Bj1DxQt5f4OrjdlgSVkZT37Z3hx5hUIN5AUDSqL/e99vgtTcqj5vnm067epZL1EF/QiqsniZO8Q1GSkItOakDg==} + '@react-native/asset-utils@0.88.1': + resolution: {integrity: sha512-BLb5FF7NgggGYiCsA7KKkK2qFhn0K+6iGXOI+4Ucd+G7Robzw9Gt4WsWwxLt+Od+G+/hyqfz8ASokyWFZ6VcLw==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} '@react-native/babel-plugin-codegen@0.88.0': @@ -1956,32 +1962,38 @@ packages: peerDependencies: '@babel/core': '*' - '@react-native/community-cli-plugin@0.88.0': - resolution: {integrity: sha512-WrQdXbjVt1gwAay8ZzYtpOi9ly99ksJ0VMYFdVcMe6nq3vtiRgqKRpHgCdN6ySIbtr5o+aJ+Vr7OTDY98ec/Ow==} + '@react-native/codegen@0.88.1': + resolution: {integrity: sha512-URPgPhnsxqeQSLyTRVzhZZ8iNXtZdR1Jop6lqaQacymvl3byghXisNIRR1YyG6PbXUfzPan5Jmz/kPhUUsZ2Yw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.88.1': + resolution: {integrity: sha512-L2CQvzsOMz4oof6AVLSzesFL1Q92nrtXpK0uQH2VwdOmzzgkiHql8WFQSo9VLvMFGJDg8+G6enrHH573ruqqWA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} peerDependencies: '@react-native-community/cli': '*' - '@react-native/metro-config': 0.88.0 + '@react-native/metro-config': 0.88.1 peerDependenciesMeta: '@react-native-community/cli': optional: true '@react-native/metro-config': optional: true - '@react-native/debugger-frontend@0.88.0': - resolution: {integrity: sha512-v5cnBaaWhDRPcQv8EC20zj53jOAKH7u2wnzzXS4JTNu+diPC+cA2+kC7rq3eKI5vGhlAcG2kDPoViRuC0AY0oA==} + '@react-native/debugger-frontend@0.88.1': + resolution: {integrity: sha512-4ZjH2093hZWWgsYs5ExqjzJX5uCS4R5yZa17eRHsQClUc6RjlV61kLdIk722sQReLL+NYtBn2RJtdcQtLCwiug==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - '@react-native/debugger-shell@0.88.0': - resolution: {integrity: sha512-p82zT8ZKNx7nIm/8zf8pi55sxm5KQBm8td47kzoJysTdqHLyPsRMUvDXtvCJIWadHSv7RJ6t/yiCc+IoImLBtw==} + '@react-native/debugger-shell@0.88.1': + resolution: {integrity: sha512-vrOLlxkp/417qcSUiS3x3gmdoQHYA/26oEKIWPB4EekJG0uJxKXsMR0tHIDiJWO0JH/2BO3M+507DjR1U74jQg==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - '@react-native/dev-middleware@0.88.0': - resolution: {integrity: sha512-sHLxVHOwfkF672ZzMepNh4d3IKbrZy3qXDKWSwN5LC5VJkzzZ5FdmSPP4N/CCn2pcKJ4oSqvojXwN4De3Xk92A==} + '@react-native/dev-middleware@0.88.1': + resolution: {integrity: sha512-0smOWE3NCdEYUwPiEdiwVXjTdvXcwSejviCM0YZB0Dj6HaQ+uu4hpSrvgERDevr5dvtSaqr8N4TdYZCLXiVgFw==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - '@react-native/gradle-plugin@0.88.0': - resolution: {integrity: sha512-4jPSKslOfJj8NKY/jv9qmWkxZYmqPyamoS3Vs0JOFxw9t4PM6L/ea0fUsjNV0LxC9zJlvWilPJOCTOf7h8wlyw==} + '@react-native/gradle-plugin@0.88.1': + resolution: {integrity: sha512-zalxoJJ/m5kxLnBy9I1zYrx4jaPxPXdys8DeROvrSOa3pSQkTwVVXRZ4FkXCdGodVA9KR2WitplDo/URNjyvFA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} '@react-native/jest-preset@0.88.0': @@ -1994,16 +2006,16 @@ packages: resolution: {integrity: sha512-Iq6v4+EWdOLv0M1+BpADQJBVfInl6FkABXCIBSi0FbvHahEYT+Y4UJ7Cm96p18gbEN1VKzqz3nbBbwLA6bx0rQ==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - '@react-native/normalize-colors@0.88.0': - resolution: {integrity: sha512-eZCu3tGAQCfvMCPQJVY4dmoMX8ZZYBvqIGHX7lwOYyzNK+Dl8e5ky7ZoXub1Rq75oSrQXNNmSP2CDjNNXXZpFQ==} + '@react-native/normalize-colors@0.88.1': + resolution: {integrity: sha512-2SAKiLyZRf+peBLkhhJWGY5vC26w1Uj9QBgUZvjAnOxjgeEKUGBE5riPS0R7+YoqSrRL0w8ucwZB4M+vvy9HMg==} - '@react-native/virtualized-lists@0.88.0': - resolution: {integrity: sha512-zpZIOAbobdS93edwHkNUdGDzvElNeQJqukRERRPns3/ucPvuMd2G4fYpmVJSVjhxtGCcLLB1PFndLYYfwaYoBQ==} + '@react-native/virtualized-lists@0.88.1': + resolution: {integrity: sha512-Zt+OGvJE6VgmzgemZL8TSA0VomNWpHMNcG0OvLkkBQEGrzY8OdDHRfjJh7S+f+UFJF77kicRC6o6mt3A7ER9lQ==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} peerDependencies: '@types/react': ^19.2.0 react: '*' - react-native: 0.88.0 + react-native: 0.88.1 peerDependenciesMeta: '@types/react': optional: true @@ -2501,6 +2513,9 @@ packages: '@types/expect@1.20.4': resolution: {integrity: sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -2525,6 +2540,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} @@ -4030,16 +4048,16 @@ packages: resolution: {integrity: sha512-f9vK751YUzD0qaHSpKc5/qQM6gqOlgGS+4ZRqTNcZOpVqn3mC55yEQ6dUG2K1eaVm5FbKkz9CFWe6XNvrkQFQg==} engines: {node: '>=18'} - flow-estree@0.331.0: - resolution: {integrity: sha512-FVLYkSL/ITb/QXBEQvNWPjPooRGswuUtGOwrH+puSlMDneNkxy640+FZsk/TfKL+b7Wbw/Fg9FTUv2bMQDpj2w==} + flow-estree@0.336.0: + resolution: {integrity: sha512-0VaGWHJourzh88otU5bVJ4Z/oIsD+R6yaeR0Q8t8dezsKQI/RLi5m3fNzkiLYLhJ+YwM3JRSKlOqGplOI5Yo3w==} engines: {node: '>=18'} flow-parser@0.327.0: resolution: {integrity: sha512-/MYZ5hXWf5ozpbdfUDmazMgQXviLxWEiRLH3MppEGDdW0itRVqmB5UaU7QX+SBCDNiq+gEesSVn8nlxmPDyDkw==} engines: {node: '>=0.4.0'} - flow-parser@0.331.0: - resolution: {integrity: sha512-vEZcHIlnKeN8JSVtYFczj8sWJfHIJOkz+YG00O/oul/Bete+t1ZhTi+O6JRRLFYvGW+yJkZ0YgL16iStnbkUqQ==} + flow-parser@0.336.0: + resolution: {integrity: sha512-O8QvQf/B3rBF+eagcc9/WMAjfJoaKckdgSd8PT/qvMIIe4ZjVKrSqV5SDnRFAKY0VrE8r5C4ADYgvYhmwkWADA==} engines: {node: '>=0.4.0'} fly-import@1.0.0: @@ -4221,8 +4239,8 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} - hermes-compiler@260318099.0.4: - resolution: {integrity: sha512-8odcPFkPC82HlqCl0C5QI9mBN4rEAT+IAo55sJhzwKw//K3qZloiv3ex6J2O8CuBbhdmkJozfidNsset2KH25w==} + hermes-compiler@260318099.0.5: + resolution: {integrity: sha512-4Gze+pGTGTUBRmnv6LtdOMo6ZzMMh3hEkvDiWzvuNL5LM0EO5YGhr8xh3fRjFaEqReUwEGjJGcGgEPFe9+E6Gw==} hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -5153,61 +5171,61 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - metro-babel-transformer@0.87.1: - resolution: {integrity: sha512-orvRIGpb0yK1yMbk5YP3dhVKjmoxUkRB9fdltGJxNppEqus/1tFpNuf9KnGFR3Qjg0izoUrJ4z6WgoBvdkXPrA==} + metro-babel-transformer@0.87.2: + resolution: {integrity: sha512-ZAfqGp0vttJFI6k1LEw//nGgTYfyVHFjcU61Ac7aPuTSaPsT/MeOi710ODfrxWkwK/y6z45NrIejjMwPv0CnxQ==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-cache-key@0.87.1: - resolution: {integrity: sha512-scqVVPMA2c+RVO12I3wyMDp5xCkRPLsyrcG4Qc3xfU5JR0EW/0sz2tjty+5UjMUtG09uiM1KYQ8HjWg7OQXHtw==} + metro-cache-key@0.87.2: + resolution: {integrity: sha512-wYtWOqRSaYlVtQe9B5YwobOFjf30IUuZZ6FqZ5cCTzisvh1varAdFCs/CGEWxBtJqe3qcgZKSi7cP4yoR0OZEQ==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-cache@0.87.1: - resolution: {integrity: sha512-juNPaj0Xi5tkLp8imXqvbSnEIijwKG36m7OiFFVJMqEkiURwAjkMFVek3/xpix1+LVQqO7kjVp3PvuvIM61mKQ==} + metro-cache@0.87.2: + resolution: {integrity: sha512-2hWZsXhEYhhzYneNH8Q/ToZZKogcHDWhXTUloiJg+p+QGLEsDx9LXadxXXHfhPDaSRc9B6VOcB0Te02dPv/tRw==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-config@0.87.1: - resolution: {integrity: sha512-NmkDlc/qZAdo5gTkVTuO2AKE/khemRKRH5IA+SKfqUBJigI5GqZ5TOXhmEokhlkAqzjt8dyzKhzI6FHvXoXaTQ==} + metro-config@0.87.2: + resolution: {integrity: sha512-uIlI0RMKHUXI5xEe+7HkWgk8KFDjCCx7o/G/rcyqlUVbjM7N/3rDwCuT++dqC4UzeC4Afv1jDbS+bFI8/gRlNA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-core@0.87.1: - resolution: {integrity: sha512-xizzky/4+c/Mkznege8OW05j7bSdM39VPoEps61Se4+hf32UQujmbvLvIE9+UnP6kNEPVomk0Fw8NsBK9iRQmw==} + metro-core@0.87.2: + resolution: {integrity: sha512-VlNETRpQXXSyj6hSU5l0xOANSAvJ81wIH/QpXcJ+nPbU6qteofQ0tsYO1HbtBIj/5MwbXjcEeDca9CJ2N40VZA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-file-map@0.87.1: - resolution: {integrity: sha512-9sPuNCojC3pS4vj+ZPY7FXmHGpdJeckXuKXQrDAfJvZc60AF8owRFTkI8XdwlwrmPIB2w++KraK9FMEXvC4wJw==} + metro-file-map@0.87.2: + resolution: {integrity: sha512-zXB6o7Oj/xVGOhHQ+dz8BIAjZqjv1S1v/sJIqqRus8XiZISNspO3DmnKWssW0Gh2XO+Sq4TAjkIuYQnBJ+lnjA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-minify-terser@0.87.1: - resolution: {integrity: sha512-YK4k1oO1wV48hfE+Nbw2rJCnAgeabzrgT/xH5ha5n9gbK4p3HYFC898cERUiyTl9vRh58CNTiXRc88f7y6PfKg==} + metro-minify-terser@0.87.2: + resolution: {integrity: sha512-+sJo/F9+Xb+duOt4xA1HgNa9FRq1dsPcdTMxVrLsT1RVvgKVuA13qMeYvhR+vdlQbuXFXIrCCDOkMphxlM2/yA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-resolver@0.87.1: - resolution: {integrity: sha512-FXH/brY69wT6sPxaSW8BhMmDGD/6irmBr/cL4rUi+VWwqR3pbvdcfUOfyjcHucgQ7pe5IW3xJY21eHKLVKNumQ==} + metro-resolver@0.87.2: + resolution: {integrity: sha512-5irxge/5uJ+OqPwdj4GARrSmJVMVj4mkgn5WKv83X7NP9MH/AedDStqvZaMwEaiH/mQFm5W4GDbEqx1OJJeLBg==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-runtime@0.87.1: - resolution: {integrity: sha512-kjeuSvInsM6OzBjkCQsWxnkrKZFMKtnkCl38oXWRI9lDGs2L9hGs9Tak9sQYJ9h3gtDujneB7wZio+irgqJKDw==} + metro-runtime@0.87.2: + resolution: {integrity: sha512-68CLgcufiI162xyhSyI3Ax1kYPmUxKV6mt7Ll6gNd6Lb5ayJ+tQ1G7G+02Hda2ekV88Vq5zTsJ5hv2qpAhZa8g==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-source-map@0.87.1: - resolution: {integrity: sha512-Bqpm0PBGdy53pigIJb4HRF7QxD2pUOdgSmVh/4AWbP635kSCv+Y6ltVxUZgSnzS/QbSmzyCGijmRPlXMj2/qHw==} + metro-source-map@0.87.2: + resolution: {integrity: sha512-6jqJ/b+Ho36JmiI1pAjSWvnuDHqLCkF6Pjv4bxyjESu+18TrqvPwOS82QXQwyrr+haoc5kVnguxrgRWFhuqbEg==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-symbolicate@0.87.1: - resolution: {integrity: sha512-rcGvwabrg0lbXGRGRKl14PY3My3+dkno2jZYhdJEuH2DmjTtTkV5bYwgto4JtV/Lbe+sXE6GO1UGg654430uow==} + metro-symbolicate@0.87.2: + resolution: {integrity: sha512-bAE3KvaYjlyOzBHE+FI7NnG57bGoW+n9Et1leevreuMXYEaIhN/CmfCh3fRuApoj5LnJg1rFlxCXaJsIc4P5Gg==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} hasBin: true - metro-transform-plugins@0.87.1: - resolution: {integrity: sha512-qMiJ/x+VfqumFJJSefKS6nj8uojzC29b2/aK8vlMuFABIqGjDucICEOXd2Yt6k/XMFAgAcaJvShUOT0HLyYaPw==} + metro-transform-plugins@0.87.2: + resolution: {integrity: sha512-pjss163wbj3G3bEANVJtzjIsHUeSP025aSlAxAla6QvwXFedD3tMDT19a9q2KRhp5OgfrDm/FbVusJuVHA0NYg==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro-transform-worker@0.87.1: - resolution: {integrity: sha512-VOzs3OV405FuOLYdAhqA2wsn1F9lvueDVxMDLvCKaQ44U+yknIqphJ9eMjjsGwLDZiJq9sW3DMLhjD27JxQgqQ==} + metro-transform-worker@0.87.2: + resolution: {integrity: sha512-DLpo4Xl2vShovtqXfB1JYJPAWsWjzuLlbmWYscOr0qpshjZ8Ii6JBDEnFBOVEzeXNyRG/gr697v61N25N9YvDg==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - metro@0.87.1: - resolution: {integrity: sha512-1oyLU9elM7hPAsKIqKooEekwxiuWr27+MZBhUROPhzcbXhMDxK64GPF/hCXwBZVp1F89ODavCrBmJtLx9WtBoQ==} + metro@0.87.2: + resolution: {integrity: sha512-SPdGRafxrx7pf2bNta9qvwfX6tFMxULEEDM5irfsPH+AinubUoLqZUkguJl6bwCZOVO4hOGgjr7esiSDqPjkoQ==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} hasBin: true @@ -5455,8 +5473,8 @@ packages: nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} - ob1@0.87.1: - resolution: {integrity: sha512-i8iA8uij0g1YQzS8uOJPSRCgwDjO9warIHUAu1Fqj877Wc3wlfxDYBioYWgKTBF2+URVJttyDWSEpmd99nlvtQ==} + ob1@0.87.2: + resolution: {integrity: sha512-KLdsz/5QTEG2Y7lhxEOYNBZX7MkD0m3Cu11B72TBJ6sCKGXYatLJ/4WtMKoGLalQyozFYDutPRpsWpDRRA+E4g==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} object-assign@4.1.1: @@ -5667,11 +5685,6 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - peggy@1.2.0: - resolution: {integrity: sha512-PQ+NKpAobImfMprYQtc4Egmyi29bidRGEX0kKjCU5uuW09s0Cthwqhfy7mLkwcB4VcgacE5L/ZjruD/kOPCUUw==} - engines: {node: '>=10'} - hasBin: true - peggy@5.1.0: resolution: {integrity: sha512-IEo5aYRZ2kXH4Qby06cjtL114PZnwLoTiA41vUmg2vPZgANn+c87m5BUurhuDr5/cu758ZlpgsAfBVx+hhO5+w==} engines: {node: '>=20'} @@ -6040,10 +6053,10 @@ packages: react-devtools-core@6.1.5: resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} - react-dom@19.3.0: - resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + react-dom@19.3.1: + resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} peerDependencies: - react: ^19.3.0 + react: ^19.3.1 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -6054,11 +6067,14 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.3.0: - resolution: {integrity: sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==} + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + + react-is@19.3.1: + resolution: {integrity: sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==} - react-native@0.88.0: - resolution: {integrity: sha512-v/wee0nYWcammMYBnSat/e3WY/5iaf9YQOMcBrfKT/wPApPPlHp94hgZovBtwhMuthR01yc/8cGn0FpFTE/C4A==} + react-native@0.88.1: + resolution: {integrity: sha512-wajWcLZ93GCN7k4Uwh4CS4L2Ah8ohcvkOXnvIYmh4Yvuqho5j3APZ9qozK+nQrHWSgauq4h8D3HN5DKK84SrHA==} engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} hasBin: true peerDependencies: @@ -6072,13 +6088,13 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - react-test-renderer@19.3.0: - resolution: {integrity: sha512-AUxgv+hRbqjRgx4xVwiWL6cvDDKPAva1VWLXQIMoevqQn9us//C5TX9LEnBGH47D0zvJpjwjxb1umzgOMvE8Wg==} + react-test-renderer@19.3.1: + resolution: {integrity: sha512-YcNewwmnZMd0wTb2nkb4hQ58NmX/u1hW2OJsNpen8CnV+jNkmEjYd1R6wJrOMSgb8bIF5HAJv4iL+S3PwNptOA==} peerDependencies: - react: ^19.3.0 + react: ^19.3.1 - react@19.3.0: - resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + react@19.3.1: + resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -6822,11 +6838,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -7343,8 +7354,12 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.11': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helper-wrap-function@7.28.6': @@ -7360,6 +7375,10 @@ snapshots: '@babel/template': 7.28.6 '@babel/types': 7.29.0 + '@babel/parser@7.29.11': + dependencies: + '@babel/types': 7.29.11 + '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 @@ -8049,6 +8068,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.11': + dependencies: + '@babel/helper-string-parser': 7.29.11 + '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@0.2.3': {} '@cspotcode/source-map-support@0.8.1': @@ -9124,7 +9148,7 @@ snapshots: '@xml-tools/parser': 1.0.11 prettier: 3.8.3 - '@react-native/asset-utils@0.88.0': {} + '@react-native/asset-utils@0.88.1': {} '@react-native/babel-plugin-codegen@0.88.0(@babel/core@7.29.0)': dependencies: @@ -9182,23 +9206,33 @@ snapshots: tinyglobby: 0.2.16 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.88.0': + '@react-native/codegen@0.88.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + flow-parser: 0.327.0 + invariant: 2.2.4 + nullthrows: 1.1.1 + tinyglobby: 0.2.16 + yargs: 17.7.2 + + '@react-native/community-cli-plugin@0.88.1': dependencies: - '@react-native/asset-utils': 0.88.0 - '@react-native/dev-middleware': 0.88.0 + '@react-native/asset-utils': 0.88.1 + '@react-native/dev-middleware': 0.88.1 commander: 12.1.0 debug: 4.4.3 invariant: 2.2.4 - metro: 0.87.1 + metro: 0.87.2 semver: 7.8.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@react-native/debugger-frontend@0.88.0': {} + '@react-native/debugger-frontend@0.88.1': {} - '@react-native/debugger-shell@0.88.0': + '@react-native/debugger-shell@0.88.1': dependencies: cross-spawn: 7.0.6 debug: 4.4.3 @@ -9206,11 +9240,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/dev-middleware@0.88.0': + '@react-native/dev-middleware@0.88.1': dependencies: '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.88.0 - '@react-native/debugger-shell': 0.88.0 + '@react-native/debugger-frontend': 0.88.1 + '@react-native/debugger-shell': 0.88.1 connect: 3.7.0 debug: 4.4.3 invariant: 2.2.4 @@ -9223,15 +9257,15 @@ snapshots: - supports-color - utf-8-validate - '@react-native/gradle-plugin@0.88.0': {} + '@react-native/gradle-plugin@0.88.1': {} - '@react-native/jest-preset@0.88.0(@babel/core@7.29.0)(react@19.3.0)': + '@react-native/jest-preset@0.88.0(@babel/core@7.29.0)(react@19.3.1)': dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/js-polyfills': 0.88.0 babel-jest: 29.7.0(@babel/core@7.29.0) jest-environment-node: 29.7.0 - react: 19.3.0 + react: 19.3.1 regenerator-runtime: 0.13.11 transitivePeerDependencies: - '@babel/core' @@ -9239,14 +9273,14 @@ snapshots: '@react-native/js-polyfills@0.88.0': {} - '@react-native/normalize-colors@0.88.0': {} + '@react-native/normalize-colors@0.88.1': {} - '@react-native/virtualized-lists@0.88.0(@types/react@19.3.0)(react-native@0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0))(react@19.3.0)': + '@react-native/virtualized-lists@0.88.1(@types/react@19.3.0)(react-native@0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1))(react@19.3.1)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 - react: 19.3.0 - react-native: 0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0) + react: 19.3.1 + react-native: 0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1) optionalDependencies: '@types/react': 19.3.0 @@ -9553,24 +9587,24 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.5.1(@types/node@22.20.1)(ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@5.9.3)))(react-native@0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0))(react-test-renderer@19.3.0(react@19.3.0))(react@19.3.0)': + '@testing-library/react-native@13.3.3(jest@30.5.1(@types/node@22.20.1)(ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@5.9.3)))(react-native@0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1))(react-test-renderer@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: jest-matcher-utils: 30.4.1 picocolors: 1.1.1 pretty-format: 30.4.1 - react: 19.3.0 - react-native: 0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0) - react-test-renderer: 19.3.0(react@19.3.0) + react: 19.3.1 + react-native: 0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1) + react-test-renderer: 19.3.1(react@19.3.1) redent: 3.0.0 optionalDependencies: jest: 30.5.1(@types/node@22.20.1)(ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@5.9.3)) - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 10.4.1 - react: 19.3.0 - react-dom: 19.3.0(react@19.3.0) + react: 19.3.1 + react-dom: 19.3.1(react@19.3.1) optionalDependencies: '@types/react': 19.3.0 '@types/react-dom': 19.3.0(@types/react@19.3.0) @@ -9642,6 +9676,11 @@ snapshots: '@types/expect@1.20.4': {} + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 22.20.1 + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.20.1 @@ -9675,6 +9714,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 22.20.1 + '@types/lodash-es@4.17.12': dependencies: '@types/lodash': 4.17.24 @@ -10985,10 +11028,6 @@ snapshots: dependencies: eslint: 10.4.0(jiti@2.7.0) - eslint-config-prettier@8.10.2(eslint@10.4.0(jiti@2.7.0)): - dependencies: - eslint: 10.4.0(jiti@2.7.0) - eslint-config-prettier@8.10.2(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) @@ -11370,15 +11409,15 @@ snapshots: flow-estree@0.327.0: {} - flow-estree@0.331.0: {} + flow-estree@0.336.0: {} flow-parser@0.327.0: dependencies: flow-estree: 0.327.0 - flow-parser@0.331.0: + flow-parser@0.336.0: dependencies: - flow-estree: 0.331.0 + flow-estree: 0.336.0 fly-import@1.0.0: dependencies: @@ -11576,7 +11615,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hermes-compiler@260318099.0.4: {} + hermes-compiler@260318099.0.5: {} hermes-estree@0.25.1: {} @@ -12959,102 +12998,99 @@ snapshots: merge2@1.4.1: {} - metro-babel-transformer@0.87.1: + metro-babel-transformer@0.87.2: dependencies: '@babel/core': 7.29.0 flow-enums-runtime: 0.0.6 - flow-parser: 0.331.0 - metro-cache-key: 0.87.1 + flow-parser: 0.336.0 + metro-cache-key: 0.87.2 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-cache-key@0.87.1: + metro-cache-key@0.87.2: dependencies: flow-enums-runtime: 0.0.6 - metro-cache@0.87.1: + metro-cache@0.87.2: dependencies: exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 https-proxy-agent: 7.0.6 - metro-core: 0.87.1 + metro-core: 0.87.2 transitivePeerDependencies: - supports-color - metro-config@0.87.1: + metro-config@0.87.2: dependencies: connect: 3.7.0 flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.87.1 - metro-cache: 0.87.1 - metro-core: 0.87.1 - metro-runtime: 0.87.1 + metro: 0.87.2 + metro-cache: 0.87.2 + metro-core: 0.87.2 + metro-runtime: 0.87.2 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro-core@0.87.1: + metro-core@0.87.2: dependencies: flow-enums-runtime: 0.0.6 lodash.throttle: 4.1.1 - metro-resolver: 0.87.1 + metro-resolver: 0.87.2 - metro-file-map@0.87.1: + metro-file-map@0.87.2: dependencies: debug: 4.4.3 fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 invariant: 2.2.4 jest-worker: 29.7.0 - micromatch: 4.0.8 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-minify-terser@0.87.1: + metro-minify-terser@0.87.2: dependencies: flow-enums-runtime: 0.0.6 terser: 5.47.1 - metro-resolver@0.87.1: + metro-resolver@0.87.2: dependencies: flow-enums-runtime: 0.0.6 - metro-runtime@0.87.1: + metro-runtime@0.87.2: dependencies: '@babel/runtime': 7.29.2 flow-enums-runtime: 0.0.6 - metro-source-map@0.87.1: + metro-source-map@0.87.2: dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.11 flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-symbolicate: 0.87.1 + metro-symbolicate: 0.87.2 nullthrows: 1.1.1 - ob1: 0.87.1 + ob1: 0.87.2 source-map: 0.5.7 - vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-symbolicate@0.87.1: + metro-symbolicate@0.87.2: dependencies: flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-source-map: 0.87.1 + metro-source-map: 0.87.2 nullthrows: 1.1.1 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-transform-plugins@0.87.1: + metro-transform-plugins@0.87.2: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -13065,58 +13101,57 @@ snapshots: transitivePeerDependencies: - supports-color - metro-transform-worker@0.87.1: + metro-transform-worker@0.87.2: dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.11 + '@babel/types': 7.29.11 flow-enums-runtime: 0.0.6 - metro: 0.87.1 - metro-babel-transformer: 0.87.1 - metro-cache: 0.87.1 - metro-cache-key: 0.87.1 - metro-minify-terser: 0.87.1 - metro-source-map: 0.87.1 - metro-transform-plugins: 0.87.1 + metro: 0.87.2 + metro-babel-transformer: 0.87.2 + metro-cache: 0.87.2 + metro-cache-key: 0.87.2 + metro-minify-terser: 0.87.2 + metro-source-map: 0.87.2 + metro-transform-plugins: 0.87.2 nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro@0.87.1: + metro@0.87.2: dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 + '@babel/parser': 7.29.11 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.11 accepts: 2.0.0 connect: 3.7.0 debug: 4.4.3 error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 - flow-parser: 0.331.0 - graceful-fs: 4.2.11 + flow-parser: 0.336.0 invariant: 2.2.4 jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.87.1 - metro-cache: 0.87.1 - metro-cache-key: 0.87.1 - metro-config: 0.87.1 - metro-core: 0.87.1 - metro-file-map: 0.87.1 - metro-resolver: 0.87.1 - metro-runtime: 0.87.1 - metro-source-map: 0.87.1 - metro-symbolicate: 0.87.1 - metro-transform-plugins: 0.87.1 - metro-transform-worker: 0.87.1 + metro-babel-transformer: 0.87.2 + metro-cache: 0.87.2 + metro-cache-key: 0.87.2 + metro-config: 0.87.2 + metro-core: 0.87.2 + metro-file-map: 0.87.2 + metro-resolver: 0.87.2 + metro-runtime: 0.87.2 + metro-source-map: 0.87.2 + metro-symbolicate: 0.87.2 + metro-transform-plugins: 0.87.2 + metro-transform-worker: 0.87.2 mime-types: 3.0.2 nullthrows: 1.1.1 serialize-error: 2.1.0 @@ -13349,7 +13384,7 @@ snapshots: nwsapi@2.2.23: {} - ob1@0.87.1: + ob1@0.87.2: dependencies: flow-enums-runtime: 0.0.6 @@ -13589,8 +13624,6 @@ snapshots: lru-cache: 11.3.6 minipass: 7.1.3 - peggy@1.2.0: {} - peggy@5.1.0: dependencies: '@peggyjs/from-mem': 3.1.3 @@ -13872,12 +13905,12 @@ snapshots: '@jest/schemas': 30.4.1 ansi-styles: 5.2.0 react-is-18: react-is@18.3.1 - react-is-19: react-is@19.3.0 + react-is-19: react-is@19.2.8 pretty-format@30.5.1: dependencies: '@jest/react-is-18': react-is@18.3.1 - '@jest/react-is-19': react-is@19.3.0 + '@jest/react-is-19': react-is@19.2.8 '@jest/schemas': 30.5.0 ansi-styles: 5.2.0 @@ -13941,9 +13974,9 @@ snapshots: - bufferutil - utf-8-validate - react-dom@19.3.0(react@19.3.0): + react-dom@19.3.1(react@19.3.1): dependencies: - react: 19.3.0 + react: 19.3.1 scheduler: 0.28.0 react-is@16.13.1: {} @@ -13952,30 +13985,32 @@ snapshots: react-is@18.3.1: {} - react-is@19.3.0: {} + react-is@19.2.8: {} + + react-is@19.3.1: {} - react-native@0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0): + react-native@0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1): dependencies: - '@react-native/asset-utils': 0.88.0 - '@react-native/codegen': 0.88.0(@babel/core@7.29.0) - '@react-native/community-cli-plugin': 0.88.0 - '@react-native/gradle-plugin': 0.88.0 - '@react-native/normalize-colors': 0.88.0 - '@react-native/virtualized-lists': 0.88.0(@types/react@19.3.0)(react-native@0.88.0(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.0))(react@19.3.0) + '@react-native/asset-utils': 0.88.1 + '@react-native/codegen': 0.88.1(@babel/core@7.29.0) + '@react-native/community-cli-plugin': 0.88.1 + '@react-native/gradle-plugin': 0.88.1 + '@react-native/normalize-colors': 0.88.1 + '@react-native/virtualized-lists': 0.88.1(@types/react@19.3.0)(react-native@0.88.1(@babel/core@7.29.0)(@types/react@19.3.0)(react@19.3.1))(react@19.3.1) anser: 1.4.10 ansi-regex: 5.0.1 base64-js: 1.5.1 flow-enums-runtime: 0.0.6 flow-parser: 0.327.0 - hermes-compiler: 260318099.0.4 + hermes-compiler: 260318099.0.5 invariant: 2.2.4 memoize-one: 5.2.1 - metro-runtime: 0.87.1 - metro-source-map: 0.87.1 + metro-runtime: 0.87.2 + metro-source-map: 0.87.2 nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 - react: 19.3.0 + react: 19.3.1 react-devtools-core: 6.1.5 react-refresh: 0.14.2 regenerator-runtime: 0.13.11 @@ -13998,13 +14033,13 @@ snapshots: react-refresh@0.14.2: {} - react-test-renderer@19.3.0(react@19.3.0): + react-test-renderer@19.3.1(react@19.3.1): dependencies: - react: 19.3.0 - react-is: 19.3.0 + react: 19.3.1 + react-is: 19.3.1 scheduler: 0.28.0 - react@19.3.0: {} + react@19.3.1: {} read-cache@1.0.0: dependencies: @@ -14802,26 +14837,6 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 30.5.1 - ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@4.9.5): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.20.1 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 4.9.5 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.15.41 - ts-node@10.9.2(@swc/core@1.15.41)(@types/node@22.20.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -14912,8 +14927,6 @@ snapshots: transitivePeerDependencies: - supports-color - typescript@4.9.5: {} - typescript@5.9.3: {} uglify-js@3.19.3: From 7d305bb95545c665f1cf3e6384300e722219baa6 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Tue, 28 Jul 2026 13:39:19 +0200 Subject: [PATCH 06/18] Add tagged template for applying ansi-styles --- packages/command-tests/lib/logger.ts | 16 ++++++++++++++++ packages/command-tests/main.ts | 27 ++++++++++----------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts index 737c2cd6..a5c24f8f 100644 --- a/packages/command-tests/lib/logger.ts +++ b/packages/command-tests/lib/logger.ts @@ -29,3 +29,19 @@ export function getWidgetLogger(index: number, ...[platform, boilerplate, lang, format(template, ...msgs) ); } + +/*** + * Template that applies the provided ansi-styles to the text. + * See `node:util.styleText` for more information. + * @example + * styled`Hi, ${"cyan"}Joe${"reset"}! You have ${"bold"}3 ${["bold", "yellow"]}pending${"reset"} messages.` + */ +export function styled(strings: TemplateStringsArray, ...styles: TextStyles[]) { + let result = ""; + let style: TextStyles = "reset"; + for (let i = 0; i < strings.length; i++) { + result += styleText(style, strings[i]); + style = styles[i] ?? "reset"; + } + return result; +} diff --git a/packages/command-tests/main.ts b/packages/command-tests/main.ts index 8969ae23..28ab43ad 100644 --- a/packages/command-tests/main.ts +++ b/packages/command-tests/main.ts @@ -7,12 +7,11 @@ import shelljs from "shelljs"; import { createInterface } from "readline/promises"; import { execAsync } from "./lib/exec.ts"; import { ensure, isDefined } from "./lib/option.ts"; -import { getWidgetLogger } from "./lib/logger.ts"; +import { getWidgetLogger, styled } from "./lib/logger.ts"; import { type Config, getWidgetName } from "./lib/config.ts"; import { repeat } from "./lib/strings.ts"; import { runTest } from "./tests/testRunner.ts"; import { ensureError } from "./lib/errors.ts"; -import { styleText } from "node:util"; const { readJson } = fsExtra; const { mkdir, rm, tempdir } = shelljs; @@ -91,13 +90,13 @@ async function main() { workDirs.push(workDir); return undefined; } catch (e) { - logger(styleText(["bold", "red"], "Stopped with error")); + logger(styled`${["bold", "red"]}Stopped with error`); const error = ensureError(e); error .toString() .split("\n") - .forEach(l => logger(styleText("red", l))); - logger(styleText(["bold", "red"], `Widget Directory ${workDir}`)); + .forEach(l => logger(styled`${"red"}%s`, l)); + logger(styled`${["bold", "red"]} Widget Directory %s`, workDir); return { config, error, workDir: workDir ?? "" }; } finally { release(); @@ -107,16 +106,12 @@ async function main() { ).filter(isDefined); console.log( - "\nFinished Testing: " + - styleText( - "bold", - styleText(failures.length > 0 ? "red" : "green", "%d Failed, ") + styleText("green", "%d Successful") - ), + styled`\nFinished Testing: ${["bold", failures.length > 0 ? "red" : "green"]}%d Failed, ${["bold", "green"]}%d Successful`, failures.length, CONFIGS.length - failures.length ); - console.log(styleText("green", "\nCreated %d temporary directories during testing"), workDirs.length); + console.log(styled`${"green"}\nCreated %d temporary directories during testing`, workDirs.length); let maxDirLength = 0, maxNameLength = 0; Object.entries(configWorkDirs) @@ -131,25 +126,23 @@ async function main() { " %s %s %s", dir + repeat(" ", maxDirLength - dir.length), name + repeat(" ", maxNameLength - name.length), - failures.some(f => f.workDir === dir) ? styleText(["bold", "red"], "❌ Error") : "" + failures.some(f => f.workDir === dir) ? styled`${["bold", "red"]}❌ Error` : "" ) ); if ( !readline.terminal || // If non-interactive, just clean up without asking. - /^y?e?s?$/i.test( - await readline.question(styleText("cyan", "Clean up test widgets? ") + styleText("gray", "(YES/no)")) - ) + /^y?e?s?$/i.test(await readline.question(styled`${"cyan"}Clean up test widgets? ${"gray"}(YES/no)`)) ) { console.log("Cleaning up temporary files"); try { rm("-rf", widgetsToolsPackagePath, ...workDirs); } catch (error) { - console.warn(styleText("yellow", `Unable to remove temporary files: ${ensureError(error).message}`)); + console.warn(styled`${"yellow"}Unable to remove temporary files: %s`, ensureError(error).message); } } else { console.log("Leaving temporary files"); } - console.log(styleText(["bold", "green"], "\nAll done!")); + console.log(styled`\n${["bold", "green"]}All done!`); } From 18ebf77db4bf8680a5d3e515d0fa5cfadbe77299 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Tue, 28 Jul 2026 16:47:17 +0200 Subject: [PATCH 07/18] Differentiate better between different configurations --- packages/command-tests/lib/logger.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts index a5c24f8f..c385b0c5 100644 --- a/packages/command-tests/lib/logger.ts +++ b/packages/command-tests/lib/logger.ts @@ -10,11 +10,11 @@ const COLORS: TextStyles[] = [ "blue", "magenta", "cyan", - "bgGreen", - "bgYellow", - "bgBlue", - "bgMagenta", - "bgCyan" + ["bold", "green"], + ["bold", "yellow"], + ["bold", "blue"], + ["bold", "magenta"], + ["bold", "cyan"] ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any From 354db3735dbfc8846571e6f90dc534379bda749a Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Tue, 28 Jul 2026 16:47:40 +0200 Subject: [PATCH 08/18] Add fallback to "reset" style when undefined --- packages/command-tests/lib/logger.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts index c385b0c5..910fe175 100644 --- a/packages/command-tests/lib/logger.ts +++ b/packages/command-tests/lib/logger.ts @@ -41,7 +41,15 @@ export function styled(strings: TemplateStringsArray, ...styles: TextStyles[]) { let style: TextStyles = "reset"; for (let i = 0; i < strings.length; i++) { result += styleText(style, strings[i]); - style = styles[i] ?? "reset"; + const nextStyle = styles[i] ?? "reset"; + if ( + typeof nextStyle === "string" || + (typeof nextStyle === "object" && "every" in nextStyle && nextStyle.every(x => typeof x === "string")) + ) { + style = nextStyle; + } else { + throw Error(`Expected a TextStyle argument, but received "${nextStyle}"`); + } } return result; } From 02af0cfe73e1d3be2a85d931e8be3efba686a20c Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Tue, 6 Oct 2026 12:31:01 +0200 Subject: [PATCH 09/18] Group widget related data --- packages/command-tests/lib/logger.ts | 4 ++-- packages/command-tests/lib/{config.ts => widget.ts} | 12 ++++++++++-- packages/command-tests/main.ts | 8 ++++---- packages/command-tests/tests/testRunner.ts | 10 +++++----- 4 files changed, 21 insertions(+), 13 deletions(-) rename packages/command-tests/lib/{config.ts => widget.ts} (55%) diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts index 910fe175..99d41b77 100644 --- a/packages/command-tests/lib/logger.ts +++ b/packages/command-tests/lib/logger.ts @@ -1,5 +1,5 @@ import { format, styleText } from "node:util"; -import { type Config, getWidgetName } from "./config.ts"; +import { type WidgetConfig, getWidgetName } from "./widget.ts"; type Arguments = F extends (...args: infer Args) => unknown ? Args : never; type TextStyles = Arguments[0]; @@ -20,7 +20,7 @@ const COLORS: TextStyles[] = [ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Logger = (template: string, ...msgs: any[]) => void; -export function getWidgetLogger(index: number, ...[platform, boilerplate, lang, version]: Config): Logger { +export function getWidgetLogger(index: number, ...[platform, boilerplate, lang, version]: WidgetConfig): Logger { const colorName = COLORS[index % COLORS.length]; return (template, ...msgs) => console.log( diff --git a/packages/command-tests/lib/config.ts b/packages/command-tests/lib/widget.ts similarity index 55% rename from packages/command-tests/lib/config.ts rename to packages/command-tests/lib/widget.ts index aaecb17d..5d79c05c 100644 --- a/packages/command-tests/lib/config.ts +++ b/packages/command-tests/lib/widget.ts @@ -1,10 +1,18 @@ -export type Config = [ + +export interface WidgetPackageJson { + version: string; + packagePath: string; + widgetName: string; + devDependencies: Record +} + +export type WidgetConfig = [ platform: "web" | "native", boilerplate: "full" | "empty", language: "ts" | "js", version: "latest" ]; -export function getWidgetName(...[platform, boilerplate, lang, version]: Config): string { +export function getWidgetName(...[platform, boilerplate, lang, version]: WidgetConfig): string { return `[${version.replace(".", "_")}_${lang}_${platform}_${boilerplate}]`; } diff --git a/packages/command-tests/main.ts b/packages/command-tests/main.ts index 28ab43ad..d425dad7 100644 --- a/packages/command-tests/main.ts +++ b/packages/command-tests/main.ts @@ -8,7 +8,7 @@ import { createInterface } from "readline/promises"; import { execAsync } from "./lib/exec.ts"; import { ensure, isDefined } from "./lib/option.ts"; import { getWidgetLogger, styled } from "./lib/logger.ts"; -import { type Config, getWidgetName } from "./lib/config.ts"; +import { type WidgetConfig, getWidgetName } from "./lib/widget.ts"; import { repeat } from "./lib/strings.ts"; import { runTest } from "./tests/testRunner.ts"; import { ensureError } from "./lib/errors.ts"; @@ -21,7 +21,7 @@ const PARALLELISM = 4; const DIR_COMMAND_TESTS = dirname(fileURLToPath(import.meta.url)); -const CONFIGS: Config[] = [ +const CONFIGS: WidgetConfig[] = [ ["web", "full", "js", "latest"], ["web", "full", "ts", "latest"], ["native", "full", "js", "latest"], @@ -60,9 +60,9 @@ async function main() { const widgetsToolsPackagePath = join(pluggableWidgetsToolsPath, ensure(packOutput.trim().split(/\n/g).pop())); const workDirs: string[] = []; - const configWorkDirs: Record = {}; + const configWorkDirs: Record = {}; const workDirSemaphore = new Semaphore(PARALLELISM); - const failures: Array<{ config: Config; error: Error; workDir: string }> = ( + const failures: Array<{ config: WidgetConfig; error: Error; workDir: string }> = ( await Promise.all( CONFIGS.map(async (config, index) => { const logger = getWidgetLogger(index, ...config); diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index 55a94f23..b9ac9bee 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -1,6 +1,5 @@ import shelljs from "shelljs"; import kill from "tree-kill"; -import { type Config, getWidgetName } from "../lib/config.ts"; import { type Logger } from "../lib/logger.ts"; import { join } from "node:path"; import { resolveModule } from "../lib/node.ts"; @@ -10,12 +9,13 @@ import { exec } from "node:child_process"; import { promisify } from "node:util"; import { Mutex } from "async-mutex"; import helpers from "yeoman-test"; +import { type WidgetPackageJson, type WidgetConfig, getWidgetName } from "../lib/widget.ts"; const { copy, existsSync, readFileSync, readJson, writeFileSync, writeJson } = fsExtra; const { ls, rm } = shelljs; export async function runTest( - config: Config, + config: WidgetConfig, { logger, yeomanMutex, @@ -169,7 +169,7 @@ export async function runTest( } } - async function checkDependenciesFiles(isNative: boolean, boilerplate: Config[1], version: Config[3]) { + async function checkDependenciesFiles(isNative: boolean, boilerplate: WidgetConfig[1], version: WidgetConfig[3]) { const dependenciesJSONFile = join(workDir, "dist", "tmp", "widgets", "dependencies.json"); const dependenciesTxtFile = join(workDir, "dist", "tmp", "widgets", "dependencies.txt"); @@ -186,8 +186,8 @@ export async function runTest( ? "@mendix/pluggable-widgets-tools" : null : boilerplate === "full" - ? "classnames" - : null; + ? "classnames" + : null; if ( packageName && From f0353cc5d3157c8136bd7b569db487c9e099a684 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Tue, 6 Oct 2026 12:31:42 +0200 Subject: [PATCH 10/18] Remove explicit any --- packages/command-tests/tests/testRunner.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index b9ac9bee..249fae77 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -35,11 +35,9 @@ export async function runTest( const [platform, boilerplate, lang, version] = config; const isNative = platform === "native"; const widgetName = getWidgetName(platform, boilerplate, lang, version); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let widgetPackageJson: any; logger(`Preparing widget...`); - await prepareWidget(); + const widgetPackageJson = await prepareWidget(); logger(`Ready to test!`); logger(`Testing linting...`); @@ -74,7 +72,7 @@ export async function runTest( logger(`Tested!`); - async function prepareWidget() { + async function prepareWidget(): Promise { const filesToRemove = ls(workDir) .filter(file => file !== "node_modules") .map(file => join(workDir, file)); @@ -116,12 +114,14 @@ export async function runTest( } logger("Preparing widget: Installing"); - widgetPackageJson = await readJson(join(workDir, "package.json")); + const widgetPackageJson = await readJson(join(workDir, "package.json")) as WidgetPackageJson; widgetPackageJson.devDependencies["@mendix/pluggable-widgets-tools"] = widgetsToolsPackagePath; await writeJson(join(workDir, "package.json"), widgetPackageJson); await execAsync("npm install --loglevel=error", workDir, logger); + + return widgetPackageJson; } async function testLint() { From c5e96a713f05991139f14d653255b53684c0ed7e Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Mon, 26 Oct 2026 10:12:05 +0100 Subject: [PATCH 11/18] Add log message when clearing out widget directory --- packages/command-tests/tests/testRunner.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index 249fae77..09dba2cf 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -78,6 +78,7 @@ export async function runTest( .map(file => join(workDir, file)); if (filesToRemove.length) { + logger("Preparing widget: Clearing workDir"); rm("-r", ...filesToRemove); } From d7c178453df1b50d687ca2ec226205c407e21e57 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 15:31:29 +0100 Subject: [PATCH 12/18] Add options to test prepackaged generator or tools --- packages/command-tests/lib/packages.ts | 54 +++++++++++++++ packages/command-tests/lib/temp.ts | 30 ++++++++ packages/command-tests/main.ts | 81 ++++++++++++---------- packages/command-tests/package.json | 2 - packages/command-tests/tests/testRunner.ts | 20 +++--- pnpm-lock.yaml | 6 -- 6 files changed, 139 insertions(+), 54 deletions(-) create mode 100644 packages/command-tests/lib/packages.ts create mode 100644 packages/command-tests/lib/temp.ts diff --git a/packages/command-tests/lib/packages.ts b/packages/command-tests/lib/packages.ts new file mode 100644 index 00000000..b0b71f69 --- /dev/null +++ b/packages/command-tests/lib/packages.ts @@ -0,0 +1,54 @@ +import { existsSync } from "node:fs"; +import { ensure, isDefined, type Option } from "./option.ts"; +import { isAbsolute, join, resolve } from "node:path"; +import { execAsync } from "./exec.ts"; +import { getTempDir, markPathTemporary } from "./temp.ts"; +import { styled } from "./logger.ts"; +import { promisify } from "node:util"; +import { exec } from "node:child_process"; + +export async function getWidgetsToolsPackagePath(commandTestDir: string, path: Option) { + if (isDefined(path)) { + console.log(styled`${"gray"}Preparing: Using prepackaged @mendix/pluggable-widgets-tools`); + if (existsSync(path)) { + return path; + } + throw Error(`ArgumentError: Path to @mendix/pluggable-widgets-tools does not exist: ${path}`); + } + + console.log(styled`${"gray"}Preparing: Packaging @mendix/pluggable-widgets-tools from source`); + const pluggableWidgetsToolsPath = join(commandTestDir, "../pluggable-widgets-tools"); + if (isAbsolute(pluggableWidgetsToolsPath) && !existsSync(pluggableWidgetsToolsPath)) { + throw Error(`ArgumentError: Unable to locate package source at '${pluggableWidgetsToolsPath}'`); + } + const { stdout: packOutput } = await promisify(exec)("pnpm pack", { cwd: pluggableWidgetsToolsPath }); + const packagePath = join(pluggableWidgetsToolsPath, ensure(packOutput.trim().split(/\n/g).pop())); + markPathTemporary(packagePath); + return packagePath; +} + +export async function getWidgetGeneratorPath(commandTestDir: string, path: Option) { + if (!isDefined(path)) { + console.log(styled`${"gray"}Preparing: Locating @mendix/generator-widget source files`); + const generatorSourcePath = join(commandTestDir, "../generator-widget/generators/app"); + if (!existsSync(generatorSourcePath)) { + throw Error(`ArgumentError: Unable to locate package source at '${generatorSourcePath}'`); + } + + return generatorSourcePath; + } + + console.log(styled`${"gray"}Preparing: Using prepackaged @mendix/generator-widget`); + const fullPath = resolve(path); + if (!existsSync(fullPath)) { + throw Error(`Prepackaged @mendix/generator-widget does not exist at path: ${fullPath}`); + } + + const workDir = await getTempDir("generator"); + const { stderr } = await promisify(exec)(`npm install ${fullPath}`, { cwd: workDir }); + const generatorPath = join(workDir, "node_modules/@mendix/generator-widget", "generators/app"); + if (!isDefined(generatorPath)) { + throw new Error(`Unable to unpack @mendix/generator-widget ${fullPath}:\n${stderr}`); + } + return generatorPath; +} diff --git a/packages/command-tests/lib/temp.ts b/packages/command-tests/lib/temp.ts new file mode 100644 index 00000000..8cc99be2 --- /dev/null +++ b/packages/command-tests/lib/temp.ts @@ -0,0 +1,30 @@ +import shelljs from "shelljs" +import { isDefined, type Option } from "./option.ts"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { mkdir, rm } from "node:fs/promises"; +import { styled } from "./logger.ts"; + +const { tempdir } = shelljs; + +const temporaryPaths: string[] = []; + +export function markPathTemporary(path: string) { + temporaryPaths.push(path); +} + +export async function cleanTemporaryFiles(): Promise { + console.log(styled`${"gray"}Cleaning %d temporary paths`, temporaryPaths.length) + await Promise.all(temporaryPaths.map(path => rm(path, { recursive: true, force: true }))) +} + +export async function getTempDir(name?: string) { + const temp = tempdir(); + let dir: Option; + while (!isDefined(dir) || existsSync(dir)) { + dir = join(temp, ["pwt_test", name, Math.floor(Math.random() * 10000)].filter(isDefined).join("_")) + } + await mkdir(dir, { recursive: true }) + markPathTemporary(dir) + return dir +} diff --git a/packages/command-tests/main.ts b/packages/command-tests/main.ts index d425dad7..aef767e1 100644 --- a/packages/command-tests/main.ts +++ b/packages/command-tests/main.ts @@ -1,20 +1,17 @@ #! /usr/bin/env node --experimental-strip-types import { Mutex, Semaphore } from "async-mutex"; -import fsExtra from "fs-extra"; -import { join, dirname } from "node:path"; +import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; -import shelljs from "shelljs"; import { createInterface } from "readline/promises"; -import { execAsync } from "./lib/exec.ts"; -import { ensure, isDefined } from "./lib/option.ts"; +import { isDefined, type Option } from "./lib/option.ts"; import { getWidgetLogger, styled } from "./lib/logger.ts"; import { type WidgetConfig, getWidgetName } from "./lib/widget.ts"; import { repeat } from "./lib/strings.ts"; import { runTest } from "./tests/testRunner.ts"; import { ensureError } from "./lib/errors.ts"; - -const { readJson } = fsExtra; -const { mkdir, rm, tempdir } = shelljs; +import { parseArgs } from "node:util"; +import { cleanTemporaryFiles, getTempDir } from "./lib/temp.ts"; +import { getWidgetGeneratorPath, getWidgetsToolsPackagePath } from "./lib/packages.ts"; const LIMIT_TESTS = !!process.env.LIMIT_TESTS; const PARALLELISM = 4; @@ -39,25 +36,52 @@ if (LIMIT_TESTS) { const readline = createInterface(process.stdin, process.stdout); const yeomanMutex = new Mutex(); -main() +const { values: options } = parseArgs({ + options: { + "pluggable-widgets-tools": { + type: "string" + }, + "generator-widget": { + type: "string" + }, + clean: { + type: "boolean" + } + } +}); + +main(options) .catch(e => { console.error(e); process.exitCode = 1; }) - .finally(() => { + .finally(async () => { + if ( + !readline.terminal || // If non-interactive, just clean up without asking. + !!options.clean || + /^y?e?s?$/i.test(await readline.question(styled`${"cyan"}Clean up test widgets? ${"gray"}(YES/no)`)) + ) { + await cleanTemporaryFiles(); + } else { + console.log("Leaving temporary files"); + } readline.close(); }); -async function main() { +interface CommandTestOptions { + "pluggable-widgets-tools"?: Option; + "generator-widget"?: Option; +} + + +async function main(options: CommandTestOptions) { console.log("Preparing..."); - const pluggableWidgetsToolsPath = join(DIR_COMMAND_TESTS, "../pluggable-widgets-tools"); - const pluggableWidgetsToolsVersion = (await readJson(join(pluggableWidgetsToolsPath, "package.json"))).version; - console.log("Preparing: Packaging @mendix/pluggable-widgets-tools version %s", pluggableWidgetsToolsVersion); - const { stdout: packOutput } = await execAsync("npm pack", pluggableWidgetsToolsPath, (m: string) => - console.log(m) + const widgetsToolsPackagePath = await getWidgetsToolsPackagePath( + DIR_COMMAND_TESTS, + options["pluggable-widgets-tools"] ); - const widgetsToolsPackagePath = join(pluggableWidgetsToolsPath, ensure(packOutput.trim().split(/\n/g).pop())); + const widgetGeneratorPath = await getWidgetGeneratorPath(DIR_COMMAND_TESTS, options["generator-widget"]); const workDirs: string[] = []; const configWorkDirs: Record = {}; @@ -72,11 +96,7 @@ async function main() { try { workDir = workDirs.pop(); if (!workDir) { - workDir = join( - index === 0 ? join(tempdir(), "spaced folder") : tempdir(), - `pwt_test_${Math.round(Math.random() * 10000)}` - ); - mkdir("-p", workDir); + workDir = await getTempDir(index === 0 ? "spaced folder" : undefined); } configWorkDirs[workDir] = config; await runTest(config, { @@ -84,6 +104,7 @@ async function main() { yeomanMutex, workDir, widgetsToolsPackagePath, + widgetGeneratorPath, isShortRun: LIMIT_TESTS, commandTestsDir: DIR_COMMAND_TESTS }); @@ -111,7 +132,7 @@ async function main() { CONFIGS.length - failures.length ); - console.log(styled`${"green"}\nCreated %d temporary directories during testing`, workDirs.length); + console.log(styled`${"green"}\nCreated %d temporary widget directories during testing`, workDirs.length); let maxDirLength = 0, maxNameLength = 0; Object.entries(configWorkDirs) @@ -130,19 +151,5 @@ async function main() { ) ); - if ( - !readline.terminal || // If non-interactive, just clean up without asking. - /^y?e?s?$/i.test(await readline.question(styled`${"cyan"}Clean up test widgets? ${"gray"}(YES/no)`)) - ) { - console.log("Cleaning up temporary files"); - try { - rm("-rf", widgetsToolsPackagePath, ...workDirs); - } catch (error) { - console.warn(styled`${"yellow"}Unable to remove temporary files: %s`, ensureError(error).message); - } - } else { - console.log("Leaving temporary files"); - } - console.log(styled`\n${["bold", "green"]}All done!`); } diff --git a/packages/command-tests/package.json b/packages/command-tests/package.json index 8fde9a6f..5e163a7c 100644 --- a/packages/command-tests/package.json +++ b/packages/command-tests/package.json @@ -13,8 +13,6 @@ "node": "^22.18.0" }, "dependencies": { - "@mendix/generator-widget": "workspace:*", - "@mendix/pluggable-widgets-tools": "workspace:*", "@prettier/plugin-xml": "^2.2.0", "@types/jest-image-snapshot": "^4.3.1", "@types/node": "^22.18.0", diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index 09dba2cf..be35a3a9 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -17,19 +17,21 @@ const { ls, rm } = shelljs; export async function runTest( config: WidgetConfig, { - logger, - yeomanMutex, - workDir, commandTestsDir, + isShortRun, + logger, + widgetGeneratorPath, widgetsToolsPackagePath, - isShortRun + workDir, + yeomanMutex, }: { - logger: Logger; - yeomanMutex: Mutex; - workDir: string; - isShortRun: boolean; commandTestsDir: string; + isShortRun: boolean; + logger: Logger; + widgetGeneratorPath: string; widgetsToolsPackagePath: string; + workDir: string; + yeomanMutex: Mutex; } ) { const [platform, boilerplate, lang, version] = config; @@ -102,7 +104,7 @@ export async function runTest( const release = await yeomanMutex.acquire(); // yeoman generator is no re-entrable :( try { const generatedWidget = await helpers - .run(resolveModule("@mendix/generator-widget")) + .run(widgetGeneratorPath) .withAnswers(promptAnswers) .withArguments("Generated") .run(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97291f3b..10a97f96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,12 +47,6 @@ importers: packages/command-tests: dependencies: - '@mendix/generator-widget': - specifier: workspace:* - version: link:../generator-widget - '@mendix/pluggable-widgets-tools': - specifier: workspace:* - version: link:../pluggable-widgets-tools '@prettier/plugin-xml': specifier: ^2.2.0 version: 2.2.0 From fe86f3b057ff583ab5b523ce07d458ea491eb292 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 15:56:10 +0100 Subject: [PATCH 13/18] Allow user to filter test cases by config options --- packages/command-tests/main.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/command-tests/main.ts b/packages/command-tests/main.ts index aef767e1..912b8d01 100644 --- a/packages/command-tests/main.ts +++ b/packages/command-tests/main.ts @@ -44,6 +44,9 @@ const { values: options } = parseArgs({ "generator-widget": { type: "string" }, + filter: { + type: "string" + }, clean: { type: "boolean" } @@ -71,9 +74,9 @@ main(options) interface CommandTestOptions { "pluggable-widgets-tools"?: Option; "generator-widget"?: Option; + filter?: Option; } - async function main(options: CommandTestOptions) { console.log("Preparing..."); @@ -83,12 +86,23 @@ async function main(options: CommandTestOptions) { ); const widgetGeneratorPath = await getWidgetGeneratorPath(DIR_COMMAND_TESTS, options["generator-widget"]); + let configsToTest = CONFIGS; + if (isDefined(options.filter)) { + const parsed = options.filter.split(","); + configsToTest = CONFIGS.filter((config: string[]) => parsed.every(option => config.includes(option))); + console.log( + styled`${"gray"}Preparing: Filter applied, running %d/%d test cases`, + configsToTest.length, + CONFIGS.length + ); + } + const workDirs: string[] = []; const configWorkDirs: Record = {}; const workDirSemaphore = new Semaphore(PARALLELISM); const failures: Array<{ config: WidgetConfig; error: Error; workDir: string }> = ( await Promise.all( - CONFIGS.map(async (config, index) => { + configsToTest.map(async (config, index) => { const logger = getWidgetLogger(index, ...config); logger("Scheduled, waiting for lock"); const [, release] = await workDirSemaphore.acquire(); @@ -129,7 +143,7 @@ async function main(options: CommandTestOptions) { console.log( styled`\nFinished Testing: ${["bold", failures.length > 0 ? "red" : "green"]}%d Failed, ${["bold", "green"]}%d Successful`, failures.length, - CONFIGS.length - failures.length + configsToTest.length - failures.length ); console.log(styled`${"green"}\nCreated %d temporary widget directories during testing`, workDirs.length); From a8f4a4a5e2060ecd8fa6794ae9c92b29997724c8 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 16:19:56 +0100 Subject: [PATCH 14/18] Remove mechanism for testing widgets not generatable with generator under test --- packages/command-tests/lib/logger.ts | 4 +- packages/command-tests/lib/widget.ts | 7 +-- packages/command-tests/main.ts | 19 +++--- packages/command-tests/tests/testRunner.ts | 71 ++++++++++------------ 4 files changed, 45 insertions(+), 56 deletions(-) diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts index 99d41b77..432d7655 100644 --- a/packages/command-tests/lib/logger.ts +++ b/packages/command-tests/lib/logger.ts @@ -20,12 +20,12 @@ const COLORS: TextStyles[] = [ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Logger = (template: string, ...msgs: any[]) => void; -export function getWidgetLogger(index: number, ...[platform, boilerplate, lang, version]: WidgetConfig): Logger { +export function getWidgetLogger(index: number, ...config: WidgetConfig): Logger { const colorName = COLORS[index % COLORS.length]; return (template, ...msgs) => console.log( "%s %s", - styleText(colorName, getWidgetName(platform, boilerplate, lang, version)), + styleText(colorName, getWidgetName(...config)), format(template, ...msgs) ); } diff --git a/packages/command-tests/lib/widget.ts b/packages/command-tests/lib/widget.ts index 5d79c05c..338ea160 100644 --- a/packages/command-tests/lib/widget.ts +++ b/packages/command-tests/lib/widget.ts @@ -9,10 +9,9 @@ export interface WidgetPackageJson { export type WidgetConfig = [ platform: "web" | "native", boilerplate: "full" | "empty", - language: "ts" | "js", - version: "latest" + language: "ts" | "js" ]; -export function getWidgetName(...[platform, boilerplate, lang, version]: WidgetConfig): string { - return `[${version.replace(".", "_")}_${lang}_${platform}_${boilerplate}]`; +export function getWidgetName(...[platform, boilerplate, lang]: WidgetConfig): string { + return `[${lang}_${platform}_${boilerplate}]`; } diff --git a/packages/command-tests/main.ts b/packages/command-tests/main.ts index 912b8d01..a83978fc 100644 --- a/packages/command-tests/main.ts +++ b/packages/command-tests/main.ts @@ -19,14 +19,14 @@ const PARALLELISM = 4; const DIR_COMMAND_TESTS = dirname(fileURLToPath(import.meta.url)); const CONFIGS: WidgetConfig[] = [ - ["web", "full", "js", "latest"], - ["web", "full", "ts", "latest"], - ["native", "full", "js", "latest"], - ["native", "full", "ts", "latest"], - ["web", "empty", "js", "latest"], - ["web", "empty", "ts", "latest"], - ["native", "empty", "js", "latest"], - ["native", "empty", "ts", "latest"] + ["web", "full", "js"], + ["web", "full", "ts"], + ["native", "full", "js"], + ["native", "full", "ts"], + ["web", "empty", "js"], + ["web", "empty", "ts"], + ["native", "empty", "js"], + ["native", "empty", "ts"] ]; if (LIMIT_TESTS) { @@ -119,8 +119,7 @@ async function main(options: CommandTestOptions) { workDir, widgetsToolsPackagePath, widgetGeneratorPath, - isShortRun: LIMIT_TESTS, - commandTestsDir: DIR_COMMAND_TESTS + isShortRun: LIMIT_TESTS }); workDirs.push(workDir); return undefined; diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index be35a3a9..d39e8357 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -9,7 +9,7 @@ import { exec } from "node:child_process"; import { promisify } from "node:util"; import { Mutex } from "async-mutex"; import helpers from "yeoman-test"; -import { type WidgetPackageJson, type WidgetConfig, getWidgetName } from "../lib/widget.ts"; +import { type WidgetPackageJson, type WidgetConfig } from "../lib/widget.ts"; const { copy, existsSync, readFileSync, readJson, writeFileSync, writeJson } = fsExtra; const { ls, rm } = shelljs; @@ -17,7 +17,6 @@ const { ls, rm } = shelljs; export async function runTest( config: WidgetConfig, { - commandTestsDir, isShortRun, logger, widgetGeneratorPath, @@ -25,7 +24,6 @@ export async function runTest( workDir, yeomanMutex, }: { - commandTestsDir: string; isShortRun: boolean; logger: Logger; widgetGeneratorPath: string; @@ -34,9 +32,8 @@ export async function runTest( yeomanMutex: Mutex; } ) { - const [platform, boilerplate, lang, version] = config; + const [platform, boilerplate, lang] = config; const isNative = platform === "native"; - const widgetName = getWidgetName(platform, boilerplate, lang, version); logger(`Preparing widget...`); const widgetPackageJson = await prepareWidget(); @@ -61,7 +58,7 @@ export async function runTest( await testRelease(); logger(`Checking dependencies files...`); - await checkDependenciesFiles(isNative, boilerplate, version); + await checkDependenciesFiles(isNative, boilerplate); logger(`Testing npm start...`); await testStart(); @@ -84,36 +81,32 @@ export async function runTest( rm("-r", ...filesToRemove); } - if (version === "latest") { - logger("Preparing widget: Generating"); - const promptAnswers = { - name: "Generated", - description: "My widget description", - organization: "com.mendix", - copyright: "Mendix 2020", - license: "Apache-2.0", - version: "1.0.0", - author: "Widget Generator", - projectPath: "./dist/MxTestProject", - programmingLanguage: lang === "ts" ? "typescript" : "javascript", - platform, - boilerplate, - hasUnitTests: true, - hasE2eTests: false - }; - const release = await yeomanMutex.acquire(); // yeoman generator is no re-entrable :( - try { - const generatedWidget = await helpers - .run(widgetGeneratorPath) - .withAnswers(promptAnswers) - .withArguments("Generated") - .run(); - await copy(join(generatedWidget.cwd, "generated"), workDir); - } finally { - release(); - } - } else { - await copy(join(commandTestsDir, "projects", widgetName), workDir); + logger("Preparing widget: Generating"); + const promptAnswers = { + name: "Generated", + description: "My widget description", + organization: "com.mendix", + copyright: "Mendix 2020", + license: "Apache-2.0", + version: "1.0.0", + author: "Widget Generator", + projectPath: "./dist/MxTestProject", + programmingLanguage: lang === "ts" ? "typescript" : "javascript", + platform, + boilerplate, + hasUnitTests: true, + hasE2eTests: false + }; + const release = await yeomanMutex.acquire(); // yeoman generator is no re-entrable :( + try { + const generatedWidget = await helpers + .run(widgetGeneratorPath) + .withAnswers(promptAnswers) + .withArguments("Generated") + .run(); + await copy(join(generatedWidget.cwd, "generated"), workDir); + } finally { + release(); } logger("Preparing widget: Installing"); @@ -172,7 +165,7 @@ export async function runTest( } } - async function checkDependenciesFiles(isNative: boolean, boilerplate: WidgetConfig[1], version: WidgetConfig[3]) { + async function checkDependenciesFiles(isNative: boolean, boilerplate: WidgetConfig[1]) { const dependenciesJSONFile = join(workDir, "dist", "tmp", "widgets", "dependencies.json"); const dependenciesTxtFile = join(workDir, "dist", "tmp", "widgets", "dependencies.txt"); @@ -185,9 +178,7 @@ export async function runTest( const dependenciesText = readFileSync(dependenciesTxtFile, "utf8"); const packageName = isNative - ? version === "latest" - ? "@mendix/pluggable-widgets-tools" - : null + ? "@mendix/pluggable-widgets-tools" : boilerplate === "full" ? "classnames" : null; From c0d9d8f2963f3422c15525929b35ecc94df1ae96 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 16:38:43 +0100 Subject: [PATCH 15/18] Allow relative paths to be passed as pwt and generator packages --- packages/command-tests/lib/packages.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/command-tests/lib/packages.ts b/packages/command-tests/lib/packages.ts index b0b71f69..34b575db 100644 --- a/packages/command-tests/lib/packages.ts +++ b/packages/command-tests/lib/packages.ts @@ -7,13 +7,22 @@ import { styled } from "./logger.ts"; import { promisify } from "node:util"; import { exec } from "node:child_process"; +/** + * Resolves a user supplied path. Package scripts are executed with the package directory as working directory, so + * relative paths are resolved against `INIT_CWD` (the directory the script was invoked from) when available. + */ +function resolveUserPath(path: string): string { + return resolve(process.env.INIT_CWD ?? process.cwd(), path); +} + export async function getWidgetsToolsPackagePath(commandTestDir: string, path: Option) { if (isDefined(path)) { console.log(styled`${"gray"}Preparing: Using prepackaged @mendix/pluggable-widgets-tools`); - if (existsSync(path)) { - return path; + const fullPath = resolveUserPath(path); + if (existsSync(fullPath)) { + return fullPath; } - throw Error(`ArgumentError: Path to @mendix/pluggable-widgets-tools does not exist: ${path}`); + throw Error(`ArgumentError: Path to @mendix/pluggable-widgets-tools does not exist: ${fullPath}`); } console.log(styled`${"gray"}Preparing: Packaging @mendix/pluggable-widgets-tools from source`); @@ -39,13 +48,13 @@ export async function getWidgetGeneratorPath(commandTestDir: string, path: Optio } console.log(styled`${"gray"}Preparing: Using prepackaged @mendix/generator-widget`); - const fullPath = resolve(path); + const fullPath = resolveUserPath(path); if (!existsSync(fullPath)) { throw Error(`Prepackaged @mendix/generator-widget does not exist at path: ${fullPath}`); } const workDir = await getTempDir("generator"); - const { stderr } = await promisify(exec)(`npm install ${fullPath}`, { cwd: workDir }); + const { stderr } = await promisify(exec)(`npm install "${fullPath}"`, { cwd: workDir }); const generatorPath = join(workDir, "node_modules/@mendix/generator-widget", "generators/app"); if (!isDefined(generatorPath)) { throw new Error(`Unable to unpack @mendix/generator-widget ${fullPath}:\n${stderr}`); From a05b3706ed38db39306f409cea1ac7285359bcc0 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 16:39:16 +0100 Subject: [PATCH 16/18] Merge command tests with build & test pipeline --- .github/workflows/TestAndBuild.yml | 57 +++++++++++++++++++++++++++ .github/workflows/TestPWTCommands.yml | 53 ------------------------- 2 files changed, 57 insertions(+), 53 deletions(-) delete mode 100644 .github/workflows/TestPWTCommands.yml diff --git a/.github/workflows/TestAndBuild.yml b/.github/workflows/TestAndBuild.yml index 31355d70..0ebff57e 100644 --- a/.github/workflows/TestAndBuild.yml +++ b/.github/workflows/TestAndBuild.yml @@ -8,6 +8,7 @@ on: paths: - 'pnpm-lock.yaml' - 'packages/**' + - '.nvmrc' - '.github/workflows/TestAndBuild.yml' pull_request: branches: @@ -16,12 +17,15 @@ on: paths: - 'pnpm-lock.yaml' - 'packages/**' + - '.nvmrc' - '.github/workflows/TestAndBuild.yml' jobs: test-build: name: "Quality Checks & Build" runs-on: ubuntu-latest + outputs: + packages: ${{ steps.prepare-artifacts.outputs.PACKAGES_NAME }} steps: - name: "Checking-out code" uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 @@ -84,3 +88,56 @@ jobs: path: artifacts/*.tgz retention-days: 5 + command-tests: + name: "Command tests (${{ matrix.os }})" + needs: test-build + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + defaults: + run: + shell: bash + steps: + - name: "Checking-out code" + uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 + with: + submodules: false + + - name: Setup pnpm + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0 + + - name: "Defining node version" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: ".nvmrc" + cache: 'pnpm' + + - name: "Installing dependencies" + run: pnpm install --filter command-tests + + - name: "Downloading artifacts" + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 + with: + name: ${{ needs.test-build.outputs.packages }} + path: artifacts + + - name: Increase number of watchers + if: startsWith(matrix.os, 'ubuntu') + run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + + - name: "Testing PWT commands" + working-directory: ./packages/command-tests + run: | + shopt -s nullglob + find_package() { + local matches=(../../artifacts/mendix-$1-*.tgz) + if [[ ${#matches[@]} -ne 1 ]]; then + echo "Expected exactly one artifact for $1, found ${#matches[@]}" >&2 + return 1 + fi + echo "${matches[0]}" + } + pwt=$(find_package pluggable-widgets-tools) + generator=$(find_package generator-widget) + pnpm start --pluggable-widgets-tools "$pwt" --generator-widget "$generator" diff --git a/.github/workflows/TestPWTCommands.yml b/.github/workflows/TestPWTCommands.yml deleted file mode 100644 index e8057227..00000000 --- a/.github/workflows/TestPWTCommands.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Script tests - -on: - push: - branches: - - master - - version/* - paths: - - 'pnpm-lock.yaml' - - 'packages/generator-widget/**' - - 'packages/pluggable-widgets-tools/**' - - 'packages/command-tests/**' - pull_request: - branches: - - master - - version/* - paths: - - 'pnpm-lock.yaml' - - 'packages/generator-widget/**' - - 'packages/pluggable-widgets-tools/**' - - 'packages/command-tests/**' - -jobs: - test: - name: "Script tests (${{ matrix.os }})" - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [windows-latest, ubuntu-latest] - steps: - - name: "Checking-out code" - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2 - with: - submodules: false - - - name: Setup pnpm - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0 - - - name: "Defining node version" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - cache: 'pnpm' - - - name: "Installing dependencies" - run: pnpm install - - - name: Increase number of watchers - if: startsWith(matrix.os, 'ubuntu') - run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p - - - name: "Testing PWT commands" - working-directory: ./packages/command-tests - run: pnpm start From b9f1562ed54da025330437ed09c80b6125b8133b Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 16:50:28 +0100 Subject: [PATCH 17/18] Fixup: Formatting --- packages/command-tests/lib/logger.ts | 6 +----- packages/command-tests/lib/temp.ts | 24 +++++++++++----------- packages/command-tests/lib/widget.ts | 9 ++------ packages/command-tests/tests/testRunner.ts | 10 +++------ 4 files changed, 18 insertions(+), 31 deletions(-) diff --git a/packages/command-tests/lib/logger.ts b/packages/command-tests/lib/logger.ts index 432d7655..225b25da 100644 --- a/packages/command-tests/lib/logger.ts +++ b/packages/command-tests/lib/logger.ts @@ -23,11 +23,7 @@ export type Logger = (template: string, ...msgs: any[]) => void; export function getWidgetLogger(index: number, ...config: WidgetConfig): Logger { const colorName = COLORS[index % COLORS.length]; return (template, ...msgs) => - console.log( - "%s %s", - styleText(colorName, getWidgetName(...config)), - format(template, ...msgs) - ); + console.log("%s %s", styleText(colorName, getWidgetName(...config)), format(template, ...msgs)); } /*** diff --git a/packages/command-tests/lib/temp.ts b/packages/command-tests/lib/temp.ts index 8cc99be2..e496bbcf 100644 --- a/packages/command-tests/lib/temp.ts +++ b/packages/command-tests/lib/temp.ts @@ -1,4 +1,4 @@ -import shelljs from "shelljs" +import shelljs from "shelljs"; import { isDefined, type Option } from "./option.ts"; import { existsSync } from "node:fs"; import { join } from "node:path"; @@ -10,21 +10,21 @@ const { tempdir } = shelljs; const temporaryPaths: string[] = []; export function markPathTemporary(path: string) { - temporaryPaths.push(path); + temporaryPaths.push(path); } export async function cleanTemporaryFiles(): Promise { - console.log(styled`${"gray"}Cleaning %d temporary paths`, temporaryPaths.length) - await Promise.all(temporaryPaths.map(path => rm(path, { recursive: true, force: true }))) + console.log(styled`${"gray"}Cleaning %d temporary paths`, temporaryPaths.length); + await Promise.all(temporaryPaths.map(path => rm(path, { recursive: true, force: true }))); } export async function getTempDir(name?: string) { - const temp = tempdir(); - let dir: Option; - while (!isDefined(dir) || existsSync(dir)) { - dir = join(temp, ["pwt_test", name, Math.floor(Math.random() * 10000)].filter(isDefined).join("_")) - } - await mkdir(dir, { recursive: true }) - markPathTemporary(dir) - return dir + const temp = tempdir(); + let dir: Option; + while (!isDefined(dir) || existsSync(dir)) { + dir = join(temp, ["pwt_test", name, Math.floor(Math.random() * 10000)].filter(isDefined).join("_")); + } + await mkdir(dir, { recursive: true }); + markPathTemporary(dir); + return dir; } diff --git a/packages/command-tests/lib/widget.ts b/packages/command-tests/lib/widget.ts index 338ea160..c148275c 100644 --- a/packages/command-tests/lib/widget.ts +++ b/packages/command-tests/lib/widget.ts @@ -1,16 +1,11 @@ - export interface WidgetPackageJson { version: string; packagePath: string; widgetName: string; - devDependencies: Record + devDependencies: Record; } -export type WidgetConfig = [ - platform: "web" | "native", - boilerplate: "full" | "empty", - language: "ts" | "js" -]; +export type WidgetConfig = [platform: "web" | "native", boilerplate: "full" | "empty", language: "ts" | "js"]; export function getWidgetName(...[platform, boilerplate, lang]: WidgetConfig): string { return `[${lang}_${platform}_${boilerplate}]`; diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index d39e8357..22c3fe78 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -22,7 +22,7 @@ export async function runTest( widgetGeneratorPath, widgetsToolsPackagePath, workDir, - yeomanMutex, + yeomanMutex }: { isShortRun: boolean; logger: Logger; @@ -110,7 +110,7 @@ export async function runTest( } logger("Preparing widget: Installing"); - const widgetPackageJson = await readJson(join(workDir, "package.json")) as WidgetPackageJson; + const widgetPackageJson = (await readJson(join(workDir, "package.json"))) as WidgetPackageJson; widgetPackageJson.devDependencies["@mendix/pluggable-widgets-tools"] = widgetsToolsPackagePath; await writeJson(join(workDir, "package.json"), widgetPackageJson); @@ -177,11 +177,7 @@ export async function runTest( const dependenciesJSON: Dependency[] = JSON.parse(readFileSync(dependenciesJSONFile, "utf8")); const dependenciesText = readFileSync(dependenciesTxtFile, "utf8"); - const packageName = isNative - ? "@mendix/pluggable-widgets-tools" - : boilerplate === "full" - ? "classnames" - : null; + const packageName = isNative ? "@mendix/pluggable-widgets-tools" : boilerplate === "full" ? "classnames" : null; if ( packageName && From bc900d5015b8efb81f0b2ece35b39aaace558911 Mon Sep 17 00:00:00 2001 From: Arjo Bruijnes Date: Wed, 28 Oct 2026 16:52:27 +0100 Subject: [PATCH 18/18] Fixup: Remove unused imports --- packages/command-tests/lib/packages.ts | 1 - packages/command-tests/tests/testRunner.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/command-tests/lib/packages.ts b/packages/command-tests/lib/packages.ts index 34b575db..f9305f30 100644 --- a/packages/command-tests/lib/packages.ts +++ b/packages/command-tests/lib/packages.ts @@ -1,7 +1,6 @@ import { existsSync } from "node:fs"; import { ensure, isDefined, type Option } from "./option.ts"; import { isAbsolute, join, resolve } from "node:path"; -import { execAsync } from "./exec.ts"; import { getTempDir, markPathTemporary } from "./temp.ts"; import { styled } from "./logger.ts"; import { promisify } from "node:util"; diff --git a/packages/command-tests/tests/testRunner.ts b/packages/command-tests/tests/testRunner.ts index 22c3fe78..44ce1e06 100644 --- a/packages/command-tests/tests/testRunner.ts +++ b/packages/command-tests/tests/testRunner.ts @@ -2,7 +2,6 @@ import shelljs from "shelljs"; import kill from "tree-kill"; import { type Logger } from "../lib/logger.ts"; import { join } from "node:path"; -import { resolveModule } from "../lib/node.ts"; import fsExtra from "fs-extra"; import { execAsync, execFailedAsync } from "../lib/exec.ts"; import { exec } from "node:child_process";