ionic
Version:
A tool for creating and developing Ionic Framework mobile apps.
304 lines (297 loc) • 17.1 kB
JavaScript
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;
;