blob: 10159f88d88a5c06d14fcb30c86e5e1b48d782fe [file] [log] [blame]
/**
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const fs = require('node:fs');
const path = require('node:path');
const cordova_util = require('./util');
const ConfigParser = require('cordova-common').ConfigParser;
const events = require('cordova-common').events;
const semver = require('semver');
const detectIndent = require('detect-indent');
const detectNewline = require('detect-newline');
const stringifyPackage = require('stringify-package');
const writeFileAtomicSync = require('write-file-atomic').sync;
exports.installPluginsFromConfigXML = installPluginsFromConfigXML;
exports.installPlatformsFromConfigXML = installPlatformsFromConfigXML;
// Install platforms looking at config.xml and package.json (if there is one).
function installPlatformsFromConfigXML (platforms, opts) {
events.emit('verbose', 'Checking for saved platforms that haven\'t been added to the project');
const installAllPlatforms = !platforms || platforms.length === 0;
const projectRoot = cordova_util.getProjectRoot();
const platformRoot = path.join(projectRoot, 'platforms');
const pkgJsonPath = path.join(projectRoot, 'package.json');
const confXmlPath = cordova_util.projectConfig(projectRoot);
const cfg = new ConfigParser(confXmlPath);
let pkgJson = {};
let indent = ' ';
let newline = '\n';
if (fs.existsSync(pkgJsonPath)) {
const fileData = fs.readFileSync(pkgJsonPath, 'utf8');
indent = detectIndent(fileData).indent;
newline = detectNewline(fileData);
pkgJson = JSON.parse(fileData);
} else {
if (cfg.packageName()) {
pkgJson.name = cfg.packageName().toLowerCase();
}
if (cfg.version()) {
pkgJson.version = cfg.version();
}
if (cfg.name()) {
pkgJson.displayName = cfg.name();
}
}
pkgJson.dependencies = pkgJson.dependencies || {};
pkgJson.devDependencies = pkgJson.devDependencies || {};
pkgJson.cordova = pkgJson.cordova || {};
pkgJson.cordova.platforms = pkgJson.cordova.platforms || [];
const pkgPlatforms = pkgJson.cordova.platforms.slice();
const pkgSpecs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies);
// Check for platforms listed in config.xml
const cfgPlatforms = cfg.getEngines();
cfgPlatforms.forEach(engine => {
const platformModule = engine.name.startsWith('cordova-') ? engine.name : `cordova-${engine.name}`;
// If package.json includes the platform, we use that config
// Otherwise, we need to add the platform to package.json
if (!pkgPlatforms.includes(engine.name) || (engine.spec && !(platformModule in pkgSpecs))) {
events.emit('info', `Platform '${engine.name}' found in config.xml... Migrating it to package.json`);
// If config.xml has a spec for the platform and package.json has
// not, add the spec to devDependencies of package.json
if (engine.spec && !(platformModule in pkgSpecs)) {
pkgJson.devDependencies[platformModule] = engine.spec;
}
if (!pkgPlatforms.includes(engine.name)) {
pkgJson.cordova.platforms.push(engine.name);
}
}
});
// Now that platforms have been updated, re-fetch them from package.json
const platformIDs = pkgJson.cordova.platforms.slice();
if (platformIDs.length !== pkgPlatforms.length) {
// We've modified package.json and need to save it
writeFileAtomicSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), { encoding: 'utf8' });
}
const specs = Object.assign({}, pkgJson.dependencies || {}, pkgJson.devDependencies);
const platformInfo = platformIDs.map(plID => ({
name: plID,
spec: specs[`cordova-${plID}`] || specs[plID]
}));
let platformName = '';
function restoreCallback (platform) {
platformName = platform.name;
const platformPath = path.join(platformRoot, platformName);
if (fs.existsSync(platformPath) || (!installAllPlatforms && !platforms.includes(platformName))) {
// Platform already exists
return Promise.resolve();
}
events.emit('log', `Discovered platform "${platformName}". Adding it to the project`);
// Install from given URL if defined or using a plugin id. If spec
// isn't a valid version or version range, assume it is the location to
// install from.
// CB-10761 If plugin spec is not specified, use plugin name
let installFrom = platform.spec || platformName;
if (platform.spec && semver.validRange(platform.spec, true)) {
installFrom = platformName + '@' + platform.spec;
}
const cordovaPlatform = require('./platform');
return cordovaPlatform('add', installFrom, opts);
}
function errCallback (error) {
// CB-10921 emit a warning in case of error
const msg = `Failed to restore platform "${platformName}". You might need to try adding it again. Error: ${error}`;
process.exitCode = 1;
events.emit('warn', msg);
return Promise.reject(error);
}
// CB-9278 : Run `platform add` serially, one platform after another
// Otherwise, we get a bug where the following line: https://github.com/apache/cordova-lib/blob/0b0dee5e403c2c6d4e7262b963babb9f532e7d27/cordova-lib/src/util/npm-helper.js#L39
// gets executed simultaneously by each platform and leads to an exception being thrown
return platformInfo.reduce(function (soFar, platform) {
return soFar.then(() => restoreCallback(platform), errCallback);
}, Promise.resolve());
}
// Returns a promise.
function installPluginsFromConfigXML (args) {
events.emit('verbose', 'Checking for saved plugins that haven\'t been added to the project');
const projectRoot = cordova_util.getProjectRoot();
const pluginsRoot = path.join(projectRoot, 'plugins');
const pkgJsonPath = path.join(projectRoot, 'package.json');
const confXmlPath = cordova_util.projectConfig(projectRoot);
let pkgJson = {};
let indent = ' ';
let newline = '\n';
if (fs.existsSync(pkgJsonPath)) {
const fileData = fs.readFileSync(pkgJsonPath, 'utf8');
indent = detectIndent(fileData).indent;
newline = detectNewline(fileData);
pkgJson = JSON.parse(fileData);
}
pkgJson.dependencies = pkgJson.dependencies || {};
pkgJson.devDependencies = pkgJson.devDependencies || {};
pkgJson.cordova = pkgJson.cordova || {};
pkgJson.cordova.plugins = pkgJson.cordova.plugins || {};
const pkgPluginIDs = Object.keys(pkgJson.cordova.plugins);
const pkgSpecs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies);
// Check for plugins listed in config.xml
const cfg = new ConfigParser(confXmlPath);
const cfgPluginIDs = cfg.getPluginIdList();
cfgPluginIDs.forEach(plID => {
// If package.json includes the plugin, we use that config
// Otherwise, we need to add the plugin to package.json
if (!pkgPluginIDs.includes(plID)) {
events.emit('info', `Plugin '${plID}' found in config.xml... Migrating it to package.json`);
const cfgPlugin = cfg.getPlugin(plID);
// If config.xml has a spec for the plugin and package.json has not,
// add the spec to devDependencies of package.json
if (cfgPlugin.spec && !(plID in pkgSpecs)) {
pkgJson.devDependencies[plID] = cfgPlugin.spec;
}
pkgJson.cordova.plugins[plID] = Object.assign({}, cfgPlugin.variables);
}
});
// Now that plugins have been updated, re-fetch them from package.json
const pluginIDs = Object.keys(pkgJson.cordova.plugins);
if (pluginIDs.length !== pkgPluginIDs.length) {
// We've modified package.json and need to save it
writeFileAtomicSync(pkgJsonPath, stringifyPackage(pkgJson, indent, newline), { encoding: 'utf8' });
}
const specs = Object.assign({}, pkgJson.dependencies, pkgJson.devDependencies);
const plugins = pluginIDs.map(plID => ({
name: plID,
spec: specs[plID],
variables: pkgJson.cordova.plugins[plID] || {}
}));
let pluginName = '';
function restoreCallback (pluginConfig) {
pluginName = pluginConfig.name;
const pluginPath = path.join(pluginsRoot, pluginName);
if (fs.existsSync(pluginPath)) {
// Plugin already exists
return Promise.resolve();
}
events.emit('log', `Discovered plugin "${pluginName}". Adding it to the project`);
// Install from given URL if defined or using a plugin id. If spec isn't a valid version or version range,
// assume it is the location to install from.
// CB-10761 If plugin spec is not specified, use plugin name
let installFrom = pluginConfig.spec || pluginName;
if (pluginConfig.spec && semver.validRange(pluginConfig.spec, true)) {
installFrom = pluginName + '@' + pluginConfig.spec;
}
// Add feature preferences as CLI variables if have any
const options = {
cli_variables: pluginConfig.variables,
searchpath: args.searchpath,
save: args.save || false
};
const plugin = require('./plugin');
return plugin('add', installFrom, options);
}
function errCallback (error) {
// CB-10921 emit a warning in case of error
const msg = `Failed to restore plugin "${pluginName}". You might need to try adding it again. Error: ${error}`;
process.exitCode = 1;
events.emit('warn', msg);
}
// CB-9560 : Run `plugin add` serially, one plugin after another
// We need to wait for the plugin and its dependencies to be installed
// before installing the next root plugin otherwise we can have common
// plugin dependencies installed twice which throws a nasty error.
return plugins.reduce(function (soFar, plugin) {
return soFar.then(() => restoreCallback(plugin), errCallback);
}, Promise.resolve());
}