UNPKG

ionic

Version:

A tool for creating and developing Ionic Framework mobile apps.

304 lines (297 loc) 17.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const format_1 = require("@ionic/cli-framework/utils/format"); const utils_fs_1 = require("@ionic/utils-fs"); const Debug = require("debug"); const color_1 = require("../../lib/color"); const cordova_res_1 = require("../../lib/cordova-res"); const errors_1 = require("../../lib/errors"); const base_1 = require("./base"); const debug = Debug('ionic:commands:cordova:resources'); const AVAILABLE_RESOURCE_TYPES = ['icon', 'splash']; class ResourcesCommand extends base_1.CordovaCommand { async getMetadata() { return { name: 'resources', type: 'project', summary: 'Automatically create icon and splash screen resources', description: ` Generate perfectly sized icons and splash screens from PNG source images for your Cordova platforms with this command. The source image for icons should ideally be at least ${color_1.strong('1024×1024px')} and located at ${color_1.strong('resources/icon.png')}. The source image for splash screens should ideally be at least ${color_1.strong('2732×2732px')} and located at ${color_1.strong('resources/splash.png')}. If you used ${color_1.input('ionic start')}, there should already be default Ionic resources in the ${color_1.strong('resources/')} directory, which you can overwrite. You can also generate platform-specific icons and splash screens by placing them in the respective ${color_1.strong('resources/<platform>/')} directory. For example, to generate an icon for Android, place your image at ${color_1.strong('resources/android/icon.png')}. For best results, the splash screen's artwork should roughly fit within a square (${color_1.strong('1200×1200px')}) at the center of the image. You can use ${color_1.strong('https://code.ionicframework.com/resources/splash.psd')} as a template for your splash screen. ${color_1.input('ionic cordova resources')} will automatically update your ${color_1.strong('config.xml')} to reflect the changes in the generated images, which Cordova then configures. This command uses the ${color_1.input('cordova-res')} utility[^cordova-res-repo] to generate resources locally. You can also login to your Ionic account and use Ionic servers to generate icons and splash screens with ${color_1.input('--no-cordova-res')}. Cordova reference documentation: - Icons: ${color_1.strong('https://cordova.apache.org/docs/en/latest/config_ref/images.html')} - Splash Screens: ${color_1.strong('https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/')} `, footnotes: [ { id: 'cordova-res-repo', url: 'https://github.com/ionic-team/cordova-res', }, ], exampleCommands: ['', ...cordova_res_1.SUPPORTED_PLATFORMS], inputs: [ { name: 'platform', summary: `The platform for which you would like to generate resources (${cordova_res_1.SUPPORTED_PLATFORMS.map(v => color_1.input(v)).join(', ')})`, }, ], options: [ { name: 'icon', summary: 'Generate icon resources', type: Boolean, aliases: ['i'], }, { name: 'splash', summary: 'Generate splash screen resources', type: Boolean, aliases: ['s'], }, { name: 'cordova-res', summary: `Do not generate resources locally; use Ionic servers`, type: Boolean, default: true, }, { name: 'force', summary: 'Force regeneration of resources', type: Boolean, aliases: ['f'], hint: color_1.weak('(--no-cordova-res)'), }, ], }; } async preRun(inputs, options, runinfo) { await this.preRunChecks(runinfo); const { promptToLogin } = await Promise.resolve().then(() => require('../../lib/session')); const isLoggedIn = this.env.session.isLoggedIn(); if (!options['cordova-res'] && !isLoggedIn) { this.env.log.warn(`You need to be logged into your Ionic account in order to run ${color_1.input(`ionic cordova resources`)}.\n`); await promptToLogin(this.env); } } async getBuildPlatforms() { const { getPlatforms } = await Promise.resolve().then(() => require('../../lib/integrations/cordova/project')); const { RESOURCES } = await Promise.resolve().then(() => require('../../lib/integrations/cordova/resources')); debug(`RESOURCES=${Object.keys(RESOURCES).length}`); const installedPlatforms = await getPlatforms(this.integration.root); debug(`installedPlatforms=${installedPlatforms.map(e => color_1.strong(e)).join(', ')}`); const buildPlatforms = Object.keys(RESOURCES).filter(p => installedPlatforms.includes(p)); debug(`buildPlatforms=${buildPlatforms.map(v => color_1.strong(v)).join(', ')}`); return buildPlatforms; } async run(inputs, options) { const platform = inputs[0] ? String(inputs[0]) : undefined; if (options['cordova-res']) { await this.runCordovaRes(platform, options); } else { await this.runResourceServer(platform, options); } } async runCordovaRes(platform, options) { if (!this.project) { throw new errors_1.FatalException(`Cannot run ${color_1.input('ionic cordova resources')} outside a project directory.`); } await cordova_res_1.runCordovaRes(this.env, cordova_res_1.createCordovaResArgs({ platform }, options), { cwd: this.project.directory }); } async runResourceServer(platform, options) { const { loadCordovaConfig } = await Promise.resolve().then(() => require('../../lib/integrations/cordova/config')); const { addResourcesToConfigXml, createImgDestinationDirectories, findMostSpecificSourceImage, getImageResources, getSourceImages, transformResourceImage, uploadSourceImage } = await Promise.resolve().then(() => require('../../lib/integrations/cordova/resources')); const { force } = options; const tasks = this.createTaskChain(); // if no resource filters are passed as arguments assume to use all. let resourceTypes = AVAILABLE_RESOURCE_TYPES.filter((type, index, array) => options[type]); resourceTypes = resourceTypes.length ? resourceTypes : AVAILABLE_RESOURCE_TYPES; // await this.checkForPlatformInstallation(platform, { promptToInstall: true }); const conf = await loadCordovaConfig(this.integration); const buildPlatforms = platform ? [platform] : await this.getBuildPlatforms(); if (buildPlatforms.length === 0) { throw new errors_1.FatalException(`No platforms detected. Please run: ${color_1.input('ionic cordova platform add')}`); } tasks.next(`Collecting resource configuration and source images`); const orientation = conf.getPreference('Orientation') || 'default'; // Convert the resource structure to a flat array then filter the array so // that it only has img resources that we need. Finally add src path to the // items that remain. let imgResources = getImageResources(this.integration.root) .filter(img => orientation === 'default' || typeof img.orientation === 'undefined' || img.orientation === orientation) .filter(img => buildPlatforms.includes(img.platform)) .filter(img => resourceTypes.includes(img.resType)); if (platform) { imgResources = imgResources.filter(img => img.platform === platform); } debug(`imgResources=${imgResources.length}`); // Create the resource directories that are needed for the images we will create const buildDirResponses = await createImgDestinationDirectories(imgResources); debug(`${color_1.ancillary('createImgDestinationDirectories')} completed: ${buildDirResponses.length}`); // Check /resources and /resources/<platform> directories for src files // Update imgResources to have their src attributes to equal the most // specific src img found let srcImagesAvailable = []; try { srcImagesAvailable = await getSourceImages(this.integration.root, buildPlatforms, resourceTypes); debug(`${color_1.ancillary('getSourceImages')} completed: (${srcImagesAvailable.map(v => color_1.strong(format_1.prettyPath(v.path))).join(', ')})`); } catch (e) { this.env.log.error(`Error in ${color_1.input('getSourceImages')}: ${e.stack ? e.stack : e}`); } imgResources = imgResources.map(img => { const mostSpecificImageAvailable = findMostSpecificSourceImage(img, srcImagesAvailable); return { ...img, imageId: mostSpecificImageAvailable && mostSpecificImageAvailable.imageId ? mostSpecificImageAvailable.imageId : undefined, }; }); debug(`imgResources=${imgResources.length}`); // If there are any imgResources that have missing images then end // processing and inform the user const missingSrcImages = imgResources.filter(img => !img.imageId); if (missingSrcImages.length > 0) { const missingImageText = missingSrcImages .reduce((list, img) => { const str = `${img.platform}/${img.resType}`; if (!list.includes(str)) { list.push(str); } return list; }, []) .map(v => `- ${color_1.strong(v)}`) .join('\n'); throw new errors_1.FatalException(`Source image files were not found for the following platforms/types:\n${missingImageText}\n\n` + `Please review ${color_1.input('--help')}`); } tasks.next(`Filtering out image resources that do not need regeneration`); const cachedSourceIds = srcImagesAvailable .filter(img => img.imageId && img.cachedId && img.imageId === img.cachedId) .map(img => img.imageId); if (!force) { const keepImgResources = await Promise.all(imgResources.map(async (img) => { if (!await utils_fs_1.pathExists(img.dest)) { return true; } return img.imageId && !cachedSourceIds.includes(img.imageId); })); imgResources = imgResources.filter((img, i) => keepImgResources[i]); if (imgResources.length === 0) { tasks.end(); this.env.log.nl(); this.env.log.info('No need to regenerate images.\n' + 'This could mean your generated images exist and do not need updating or your source files are unchanged.\n\n' + `You can force image regeneration with the ${color_1.input('--force')} option.`); throw new errors_1.FatalException('', 0); } } const uploadTask = tasks.next(`Uploading source images to prepare for transformations`); let count = 0; // Upload images to service to prepare for resource transformations const imageUploadResponses = await Promise.all(srcImagesAvailable.map(async (srcImage) => { const response = await uploadSourceImage(this.env, srcImage); count += 1; uploadTask.msg = `Uploading source images to prepare for transformations: ${color_1.strong(`${count} / ${srcImagesAvailable.length}`)} complete`; return response; })); debug(`${color_1.ancillary('uploadSourceImages')} completed: responses=%O`, imageUploadResponses); srcImagesAvailable = srcImagesAvailable.map((img, index) => { return { ...img, width: imageUploadResponses[index].Width, height: imageUploadResponses[index].Height, vector: imageUploadResponses[index].Vector, }; }); debug('srcImagesAvailable=%O', srcImagesAvailable); // If any images are asking to be generated but are not of the correct size // inform the user and continue on. const imagesTooLargeForSource = imgResources.filter(img => { const resourceSourceImage = srcImagesAvailable.find(srcImage => srcImage.imageId === img.imageId); if (!resourceSourceImage) { return true; } return !resourceSourceImage.vector && (img.width > resourceSourceImage.width || img.height > resourceSourceImage.height); }); debug('imagesTooLargeForSource=%O', imagesTooLargeForSource); // Remove all images too large for transformations imgResources = imgResources.filter(img => { return !imagesTooLargeForSource.find(tooLargeForSourceImage => img.name === tooLargeForSourceImage.name); }); if (imgResources.length === 0) { tasks.end(); this.env.log.nl(); this.env.log.info('No need to regenerate images--images too large for transformation.'); // TODO: improve messaging throw new errors_1.FatalException('', 0); } // Call the transform service and output images to appropriate destination const generateTask = tasks.next(`Generating platform resources`); count = 0; const transforms = imgResources.map(async (img) => { const result = await transformResourceImage(this.env, img); count += 1; generateTask.msg = `Generating platform resources: ${color_1.strong(`${count} / ${imgResources.length}`)} complete`; return result; }); const transformResults = await Promise.all(transforms); generateTask.msg = `Generating platform resources: ${color_1.strong(`${imgResources.length} / ${imgResources.length}`)} complete`; debug('transforms completed'); const transformErrors = transformResults.map(result => result.error).filter((err) => typeof err !== 'undefined'); if (transformErrors.length > 0) { throw new errors_1.FatalException(`Encountered ${transformErrors.length} error(s) during image transforms:\n\n` + transformErrors.map((err, i) => `${i + 1}): ` + color_1.failure(err.toString())).join('\n\n')); } await Promise.all(transformResults.map(async (result) => { await utils_fs_1.copy(result.tmpDest, result.resource.dest); debug('copied transformed image %s into project as %s', result.tmpDest, result.resource.dest); })); await Promise.all(srcImagesAvailable.map(async (img) => { await utils_fs_1.cacheFileChecksum(img.path, img.imageId); })); tasks.next(`Modifying config.xml to add new image resources`); const imageResourcesForConfig = imgResources.reduce((rc, img) => { if (!rc[img.platform]) { rc[img.platform] = { [img.resType]: { images: [], nodeName: '', nodeAttributes: [], }, }; } if (!rc[img.platform][img.resType]) { rc[img.platform][img.resType] = { images: [], nodeName: '', nodeAttributes: [], }; } rc[img.platform][img.resType].images.push({ name: img.name, width: img.width, height: img.height, density: img.density, }); rc[img.platform][img.resType].nodeName = img.nodeName; rc[img.platform][img.resType].nodeAttributes = img.nodeAttributes; return rc; }, {}); const platformList = Object.keys(imageResourcesForConfig); await addResourcesToConfigXml(conf, platformList, imageResourcesForConfig); tasks.end(); // All images that were not processed if (imagesTooLargeForSource.length > 0) { const imagesTooLargeForSourceMsg = imagesTooLargeForSource .map(img => ` ${color_1.strong(img.name)} ${img.platform}/${img.resType} needed ${img.width}×${img.height}px`) .concat((imagesTooLargeForSource.length > 0) ? `\nThe following images were not created because their source image was too small:` : []) .reverse(); this.env.log.rawmsg(imagesTooLargeForSourceMsg.join('\n')); } await conf.save(); } } exports.ResourcesCommand = ResourcesCommand;