@mendix/pluggable-widgets-tools
Version:
Mendix Pluggable Widgets Tools
281 lines (253 loc) • 13.5 kB
JavaScript
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
};