lib/prepare.js (221 lines of code) (raw):

/* 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('fs-extra'); const path = require('path'); const { ConfigParser, xmlHelpers, events, CordovaError } = require('cordova-common'); const ManifestJsonParser = require('./ManifestJsonParser'); const PackageJsonParser = require('./PackageJsonParser'); const SettingJsonParser = require('./SettingJsonParser'); module.exports.prepare = function (cordovaProject, options) { // First cleanup current config and merge project's one into own const defaultConfigPath = path.join(this.locations.platformRootDir, 'cordova', 'defaults.xml'); const ownConfigPath = this.locations.configXml; const sourceCfg = cordovaProject.projectConfig; // If defaults.xml is present, overwrite platform config.xml with it. // Otherwise save whatever is there as defaults so it can be // restored or copy project config into platform if none exists. if (fs.existsSync(defaultConfigPath)) { this.events.emit('verbose', `Generating config.xml from defaults for platform "${this.platform}"`); fs.copySync(defaultConfigPath, ownConfigPath); } else if (fs.existsSync(ownConfigPath)) { this.events.emit('verbose', `Generating defaults.xml from own config.xml for platform "${this.platform}"`); fs.copySync(ownConfigPath, defaultConfigPath); } else { this.events.emit('verbose', `case 3 "${this.platform}"`); fs.copySync(sourceCfg.path, ownConfigPath); } // merge our configs this.config = new ConfigParser(ownConfigPath); xmlHelpers.mergeXml(sourceCfg.doc.getroot(), this.config.doc.getroot(), this.platform, true); this.config.write(); // Update own www dir with project's www assets and plugins' assets and js-files this.parser.update_www(cordovaProject, this.locations); // Update icons updateIcons(cordovaProject, this.locations); // Update splash screens updateSplashScreens(cordovaProject, this.config, this.locations); // Copy or Create manifest.json const srcManifestPath = path.join(cordovaProject.locations.www, 'manifest.json'); if (fs.existsSync(srcManifestPath)) { // just blindly copy it to our output/www // todo: validate it? ensure all properties we expect exist? const manifestPath = path.join(this.locations.www, 'manifest.json'); this.events.emit('verbose', `Copying ${srcManifestPath} => ${manifestPath}`); fs.copySync(srcManifestPath, manifestPath); } else { this.events.emit('verbose', `Creating new manifest file in => ${this.path}`); (new ManifestJsonParser(this.locations.www)) .configure(this.config) .write(); } (new PackageJsonParser(this.locations.www, cordovaProject.root)) .configure(this.config) .enableDevTools(options && options.options && !options.options.release) .write(); const userElectronSettings = cordovaProject.projectConfig.getPlatformPreference('ElectronSettingsFilePath', 'electron'); const userElectronSettingsPath = userElectronSettings && fs.existsSync(path.resolve(cordovaProject.root, userElectronSettings)) ? path.resolve(cordovaProject.root, userElectronSettings) : undefined; // update Electron settings in .json file (new SettingJsonParser(this.locations.www)) .configure(this.config, options.options, userElectronSettingsPath) .write(); // update project according to config.xml changes. return this.parser.update_project(this.config, options); }; /** * Update Electron Splash Screen image. */ function updateSplashScreens (cordovaProject, config, locations) { const splashScreens = cordovaProject.projectConfig.getSplashScreens('electron'); // Skip if there are no splash screens defined in config.xml if (!splashScreens.length) { events.emit('verbose', 'This app does not have splash screens defined.'); return; } const splashScreen = prepareSplashScreens(splashScreens); const resourceMap = createResourceMap(cordovaProject, locations, splashScreen); updatePathToSplashScreen(config, locations, resourceMap); events.emit('verbose', 'Updating splash screens'); copyResources(cordovaProject.root, resourceMap); } /** * Get splashScreen image. Choose only one image, if the user provided multiple. */ function prepareSplashScreens (splashScreens) { let splashScreen; // choose one icon for a target const chooseOne = (defaultSplash, splash) => { events.emit('verbose', `Found extra splash screen image: ${defaultSplash.src} and ignoring in favor of ${splash.src}.`); defaultSplash = splash; return defaultSplash; }; // iterate over remaining icon elements to find the icons for the app and installer for (const image of splashScreens) { image.extension = path.extname(image.src); splashScreen = splashScreen ? chooseOne(splashScreen, image) : image; } return { splashScreen }; } /** * Update path to Splash Screen in the config.xml */ function updatePathToSplashScreen (config, locations, resourceMap) { const elementKeys = Object.keys(resourceMap[0]); const splashScreenPath = resourceMap[0][elementKeys]; const splash = config.doc.find('splash'); const preferences = config.doc.findall('preference'); splash.attrib.src = path.relative(locations.www, splashScreenPath); for (const preference of preferences) { if (preference.attrib.name === 'SplashScreen') { preference.attrib.value = splash.attrib.src; } } config.write(); } /** * Update Electron App and Installer icons. */ function updateIcons (cordovaProject, locations) { const icons = cordovaProject.projectConfig.getIcons('electron'); // Skip if there are no app defined icons in config.xml if (!icons.length) { events.emit('verbose', 'This app does not have icons defined'); return; } const filteredIcons = icons.filter(icon => checkIconsAttributes(icon)); if (!filteredIcons.length) { throw new CordovaError('No icon match the required size. Please ensure that ".png" icon is at least 512x512 and has a src attribute.'); } const chosenIcons = prepareIcons(filteredIcons); const resourceMap = createResourceMap(cordovaProject, locations, chosenIcons); events.emit('verbose', 'Updating icons'); copyResources(cordovaProject.root, resourceMap); } /** * Check if all required attributes are set. */ function checkIconsAttributes (icon) { if (((icon.height && icon.width) >= 512 || (icon.height && icon.width) === undefined) && icon.src) return true; events.emit('info', `The following${icon.target ? ` ${icon.target}` : ''} icon with a size of width=${icon.width} height=${icon.height} does not meet the requirements and will be ignored.`); return false; } /** * Find and select icons for the app and installer. * Also, set high resolution icons, if provided by a user. */ function prepareIcons (icons) { let customIcon; let appIcon; let installerIcon; // choose one icon for a target const chooseOne = (defaultIcon, icon) => { events.emit('verbose', `Found extra icon for target ${icon.target}: ${defaultIcon.src} and ignoring in favor of ${icon.src}.`); defaultIcon = icon; return defaultIcon; }; // find if there is high resolution images that has DPI suffix const highResAndRemainingIcons = findHighResIcons(icons); const highResIcons = highResAndRemainingIcons.highResIcons; const remainingIcons = highResAndRemainingIcons.remainingIcons; // iterate over remaining icon elements to find the icons for the app and installer for (const icon of remainingIcons) { const size = icon.width || icon.height; icon.extension = path.extname(icon.src); switch (icon.target) { case 'app': appIcon = appIcon ? chooseOne(appIcon, icon) : icon; break; case 'installer': installerIcon = installerIcon ? chooseOne(installerIcon, icon) : icon; break; case undefined: if ((size >= 512 || size === undefined) && !Object.keys(highResIcons).length) { customIcon = customIcon ? chooseOne(customIcon, icon) : icon; } break; } } return { customIcon, appIcon, installerIcon, highResIcons }; } /** * Find and high resolution icons and return remaining icons, * unless an icon has a specified target. */ function findHighResIcons (icons) { // find icons that are not in the highResIcons, unless they have a target set const findRemainingIcons = (icons, highResIcons) => icons.filter( (icon) => (icon.target || (!icon.target && !highResIcons.includes(icon))) ? Object.assign(icon) : false ); const highResIcons = icons.filter(icon => { // eslint-disable-line array-callback-return if (icon.src.includes('@')) { const extension = path.extname(icon.src); const suffix = icon.src.split('@').pop().slice(0, -extension.length); return Object.assign(icon, { suffix, extension }); } }); let remainingIcons = findRemainingIcons(icons, highResIcons); // set normal image that has standard resolution const has1x = highResIcons.find(obj => obj.suffix === '1x'); if (!has1x && Object.keys(highResIcons).length) { const highResIcon = highResIcons[Object.keys(highResIcons)[0]]; let baseIcon = remainingIcons.find(obj => obj.src === highResIcon.src.split('@')[0] + highResIcon.extension); if (!baseIcon) { throw new CordovaError('Base icon for high resolution images was not found.'); } const extension = path.extname(baseIcon.src); const suffix = '1x'; baseIcon = Object.assign(baseIcon, { suffix, extension }); highResIcons.push(baseIcon); remainingIcons = findRemainingIcons(icons, highResIcons); } return { highResIcons, remainingIcons }; } /** * Map resources to the appropriate target directory and name. */ function createResourceMap (cordovaProject, locations, resources) { const resourceMap = []; for (const key in resources) { const resource = resources[key]; if (!resource) { continue; } let targetPath; switch (key) { case 'customIcon': // Copy icon for the App targetPath = path.join(locations.www, 'img', `app${resource.extension}`); resourceMap.push(mapResources(cordovaProject.root, resource.src, targetPath)); // Copy icon for the Installer targetPath = path.join(locations.buildRes, `installer${resource.extension}`); resourceMap.push(mapResources(cordovaProject.root, resource.src, targetPath)); break; case 'appIcon': targetPath = path.join(locations.www, 'img', `app${resource.extension}`); resourceMap.push(mapResources(cordovaProject.root, resource.src, targetPath)); break; case 'installerIcon': targetPath = path.join(locations.buildRes, `installer${resource.extension}`); resourceMap.push(mapResources(cordovaProject.root, resource.src, targetPath)); break; case 'highResIcons': for (const key in resource) { const highResIcon = resource[key]; targetPath = path.join(locations.www, 'img', 'icon'); targetPath += highResIcon.suffix === '1x' ? highResIcon.extension : `@${highResIcon.suffix}${highResIcon.extension}`; resourceMap.push(mapResources(cordovaProject.root, highResIcon.src, targetPath)); } break; case 'splashScreen': targetPath = path.join(locations.www, '.cdv', `splashScreen${resource.extension}`); resourceMap.push(mapResources(cordovaProject.root, resource.src, targetPath)); break; } } return resourceMap; } /** * Get a map containing resources of a specified name (or directory) to the target directory. */ function mapResources (rootDir, sourcePath, targetPath) { return fs.existsSync(path.join(rootDir, sourcePath)) ? { [sourcePath]: targetPath } : {}; } /** * Copy resources to the target destination according to the resource map. */ function copyResources (rootDir, resourceMap) { resourceMap.forEach(element => { const elementKeys = Object.keys(element); if (elementKeys.length) { const value = elementKeys.map((e) => element[e])[0]; fs.copySync(path.join(rootDir, elementKeys[0]), value); } }); }