UNPKG

@mendix/pluggable-widgets-tools

Version:
281 lines (253 loc) 13.5 kB
const { join } = require("path"); const { writeJson } = require("fs-extra"); const { execSync } = require("child_process"); const { red, green, yellow, whiteBright, bold } = require("ansi-colors"); const { copyFileSync, existsSync, mkdirSync, promises } = require("fs"); const { parseStringPromise } = require("xml2js"); const { rm } = require("fs/promises"); const { auditPluggableWidgetsTools } = require("../dist/commands/audit.js"); const { confirm } = require("../dist/cli/confirm.js"); const { toolsRoot, widgetRoot } = require("../dist/widget/paths.js"); const widgetPackage = require("../dist/widget/package.js"); let requirePatch = false; const CheckType = { MAJOR: (oldVersion, newVersion) => oldVersion[0] < newVersion[0], MINOR: (oldVersion, newVersion) => oldVersion[1] < newVersion[1], MAJOR_MINOR: (oldVersion, newVersion) => oldVersion[0] < newVersion[0] || (oldVersion[0] === newVersion[0] && oldVersion[1] < newVersion[1]) }; const dependencies = [ { name: "react", version: "remove", check: CheckType.MAJOR_MINOR }, { name: "react-dom", version: "remove", check: CheckType.MAJOR_MINOR }, { name: "react-native", version: "remove", check: CheckType.MINOR }, { name: "@types/jest", version: "^29.0.0", check: CheckType.MAJOR }, { name: "@types/react", version: "remove", check: CheckType.MAJOR }, { name: "@types/react-native", version: "remove", check: CheckType.MINOR }, { name: "@types/react-native-push-notification", version: "8.1.1", check: CheckType.MAJOR_MINOR }, { name: "@types/react-dom", version: "remove", check: CheckType.MAJOR }, { name: "@types/react-test-renderer", version: "19.0.0", check: CheckType.MAJOR }, { name: "@types/enzyme-adapter-react-16", version: "remove", check: CheckType.MAJOR }, { name: "@react-native-firebase/app", version: "17.3.0", check: CheckType.MAJOR_MINOR }, { name: "@react-native-firebase/messaging", version: "17.3.0", check: CheckType.MAJOR_MINOR }, { name: "react-native-camera", version: "3.40.0", check: CheckType.MAJOR_MINOR, patch: "react-native-camera+3.40.0.patch" }, { name: "react-native-gesture-handler", version: "1.10.3", check: CheckType.MAJOR_MINOR, patch: "react-native-gesture-handler+1.10.3.patch" }, { name: "react-native-image-picker", version: "5.0.1", check: CheckType.MAJOR }, { name: "react-native-maps", version: "0.31.1", check: CheckType.MAJOR_MINOR }, { name: "react-native-progress", version: "^5.0.0", check: CheckType.MAJOR }, { name: "react-native-push-notification", version: "8.1.1", check: CheckType.MAJOR_MINOR }, { name: "react-native-webview", version: "11.26.1", check: CheckType.MAJOR_MINOR } ]; const reactPackage = { version: "19.0.0", check: CheckType.MAJOR_MINOR }; const reactDomPackage = { version: "19.0.0", check: CheckType.MAJOR_MINOR }; const reactNativePackage = { version: "0.78.2", check: CheckType.MINOR }; const resolutionsOverrides = [ { name: "react", ...reactPackage }, { name: "react-dom", ...reactDomPackage }, { name: "react-native", ...reactNativePackage }, { name: "@types/react", ...reactPackage }, { name: "@types/react-dom", ...reactDomPackage }, { name: "@types/react-native", ...reactNativePackage } ]; function extractVersions(version) { return version.replace(/^\D+/, "").split(".").map(Number); } function getOutdatedDependencies(packageDependencies, listOfNewDependencies = dependencies) { return listOfNewDependencies .filter(dep => !!packageDependencies[dep.name]) .map(dep => ({ dep, oldVersion: extractVersions(packageDependencies[dep.name]), newVersion: dep.version !== "remove" ? extractVersions(dep.version) : undefined })) .filter(({ dep, oldVersion, newVersion }) => newVersion === undefined || dep.check(oldVersion, newVersion)) .map(({ dep }) => ({ name: dep.name, oldVersion: packageDependencies[dep.name], newVersion: dep.version, patch: dep.patch })); } function replaceOldDependencies(listOfOutdatedDependencies, packageJson, key) { if (listOfOutdatedDependencies.length > 0) { console.log(green(`The following ${key} were updated:`)); listOfOutdatedDependencies.forEach(dep => { if (dep.newVersion === "remove") { delete packageJson[key][dep.name]; console.log(green(`${dep.name}: ${red(dep.oldVersion)} -> ${yellow("(removed)")}`)); } else { packageJson[key][dep.name] = dep.newVersion; if (dep.patch) { const dir = join(widgetRoot, "patches"); if (!existsSync(dir)) { mkdirSync(dir); } copyFileSync(join(toolsRoot, "./patches", dep.patch), join(widgetRoot, "patches", dep.patch)); requirePatch = true; } console.log(green(`${dep.name}: ${red(dep.oldVersion)} -> ${dep.newVersion}`)); } }); } } async function addExtraDependencies(packageJson, key) { const dependenciesToAdd = await getExtraDependencies(packageJson, key); if (dependenciesToAdd.length > 0) { console.log(green(`The following ${key} were added:`)); packageJson[key] = packageJson[key] || {}; dependenciesToAdd.forEach(dep => { packageJson[key][dep.name] = dep.version; console.log(green(`${dep.name}: ${dep.version}`)); }); } } async function getExtraDependencies(packageJson, key) { const rawPackageXML = await promises.readFile(join(widgetRoot, "src/package.xml"), "utf-8"); if (!rawPackageXML) { throw new Error("package.xml file was not found, please check your src folder"); } const parsedPackageXML = await parseStringPromise(rawPackageXML); if (!parsedPackageXML) { throw new Error("Empty package.xml file, please check your src folder"); } const widgetDefinitionXMLPaths = parsedPackageXML.package.clientModule[0].widgetFiles .map(wf => wf.widgetFile) .reduce((a, e) => a.concat(e), []) .filter(wfXml => wfXml.$.path); if (widgetDefinitionXMLPaths.length === 0) throw new Error( "Path to the widget definition XML file(s) could not be found, please check your package.xml file" ); const parsedWidgetDefinitionXMLs = []; for (const widgetDefinitionXMLPath of widgetDefinitionXMLPaths) { const rawWidgetDefinitionXML = await promises.readFile( join(widgetRoot, "src/", widgetDefinitionXMLPath.$.path), "utf-8" ); if (!rawWidgetDefinitionXML) { throw new Error( `Widget definition XML file (with path ${widgetDefinitionXMLPath}) could not be found, please check your src folder` ); } const parsedWidgetDefinitionXML = await parseStringPromise(rawWidgetDefinitionXML); if (!parsedWidgetDefinitionXML) { throw new Error( `Widget definition XML file (with path ${widgetDefinitionXMLPath}) is empty, please check your src folder` ); } parsedWidgetDefinitionXMLs.push(parsedWidgetDefinitionXML); } const supportedPlatforms = []; for (const parsedWidgetDefinitionXML of parsedWidgetDefinitionXMLs) { const supportedPlatform = parsedWidgetDefinitionXML.widget.$.supportedPlatform; if (!supportedPlatform) { throw new Error("supportedPlatform attribute is missing, please check your widget definition XML file"); } supportedPlatforms.push(supportedPlatform); } let extraDependencies = resolutionsOverrides.filter(ov => !packageJson[key] || !packageJson[key][ov.name]); if (!supportedPlatforms.includes("Native")) extraDependencies = extraDependencies.filter( d => d.name !== "react-native" && d.name !== "@types/react-native" ); if (!supportedPlatforms.includes("Web")) extraDependencies = extraDependencies.filter(d => d.name !== "react-dom" && d.name !== "@types/react-dom"); return extraDependencies; } async function checkMigration() { const paths = { packageJson: widgetPackage.path, packageLock: join(widgetRoot, "package-lock.json"), node_modules: join(widgetRoot, "node_modules") }; console.log("Checking if dependencies should be migrated..."); const packageJson = widgetPackage.json; const args = process.argv; if (!args.includes("--skip-migration") && process.env.CI !== "true") { const outdatedDependencies = getOutdatedDependencies(packageJson.dependencies || {}); const outdatedDevDependencies = getOutdatedDependencies(packageJson.devDependencies || {}); const outdatedOverrides = getOutdatedDependencies(packageJson.overrides || {}, resolutionsOverrides); const outdatedResolutions = getOutdatedDependencies(packageJson.resolutions || {}, resolutionsOverrides); if ( outdatedDependencies.length > 0 || outdatedDevDependencies.length > 0 || outdatedOverrides.length > 0 || outdatedResolutions.length > 0 ) { console.log( yellow( "Your widget contains outdated dependencies that will not work with this version of Pluggable Widgets Tools.\n" + (existsSync(paths.packageLock) ? "Note: To ensure a clean upgrade, a force-install is performed. Removing the lockfile and node_modules before installation." : "Note: To ensure a clean upgrade, a force-install is recommended. Removing the lockfile and node_modules before installation.") ) ); if (await confirm("Update dependencies?")) { try { const newPackageJson = packageJson; replaceOldDependencies(outdatedDependencies, newPackageJson, "dependencies"); replaceOldDependencies(outdatedDevDependencies, newPackageJson, "devDependencies"); replaceOldDependencies(outdatedOverrides, newPackageJson, "overrides"); replaceOldDependencies(outdatedResolutions, newPackageJson, "resolutions"); // We check if any dependency should be added in overrides/resolutions await addExtraDependencies(newPackageJson, "overrides"); await addExtraDependencies(newPackageJson, "resolutions"); // If any package requires a patch we make sure to install patch-package and add the script if (requirePatch) { newPackageJson.devDependencies["patch-package"] ||= "^6.5.0"; if (!newPackageJson.scripts.postinstall) { newPackageJson.scripts.postinstall = "patch-package"; } else if (!newPackageJson.scripts.postinstall.includes("patch-package")) { newPackageJson.scripts.postinstall = "patch-package && " + newPackageJson.scripts.postinstall; } } // Writes the new package keeping the current format await writeJson(paths.packageJson, newPackageJson, { spaces: 2 }); // Auto install is only implemented for npm right now if (!existsSync(paths.packageLock)) { console.log( "Dependency versions have been updated in the package.json of the widget.\n" + bold( whiteBright( "Force install dependencies with your package manager to complete the update." ) ) ); return; } console.log("Deleting old dependencies..."); await rm(paths.node_modules, { recursive: true, force: true }); await rm(paths.packageLock); console.log("Installing dependencies..."); execSync(`npm install`, { cwd: widgetRoot, stdio: "inherit" }); execSync( `npx eslint --no-config-lookup --rule '@typescript-eslint/no-unused-vars: ["error", { enableAutofixRemoval: { imports: true } }]' --ext ts,tsx,js,jsx --parser @typescript-eslint/parser --plugin '@typescript-eslint' --fix ./src`, { cwd: widgetRoot, stdio: "inherit" } ); await auditPluggableWidgetsTools(); console.log(green("Done auto-updating dependencies")); } catch (e) { console.log(red("An error occurred while auto updating your dependencies")); console.error(e); } } } else { console.log(green("Dependencies up-to-date.")); } } else { console.log(yellow("Skipping dependency migration")); } } module.exports = { checkMigration };