From 6b1034e757096615badc49119cb25e34ed7942ad Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:43:16 +0100 Subject: [PATCH 1/2] feat: generate banners for androidTV --- README.md | 3 +- src/definitions.ts | 6 ++ src/platforms/android/assets.ts | 50 +++++++++++ src/platforms/android/index.ts | 130 ++++++++++++++++++++++++++- src/project.ts | 1 + test/platforms/android.asset.test.ts | 2 +- 6 files changed, 189 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a821c5c..fc07ad4 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,14 @@ Where the provided flags are: ### Usage - Custom Mode -This mode provides full control over the assets used to generate icons and splash screens, but requires more source files. To use this mode, provide custom icons and splash screen source images as shown below: +This mode provides full control over the assets used to generate icons, banners and splash screens, but requires more source files. To use this mode, provide custom icons, banners and splash screen source images as shown below: ``` assets/ ├── icon-only.png ├── icon-foreground.png ├── icon-background.png +├── banner.png ├── splash.png └── splash-dark.png ``` diff --git a/src/definitions.ts b/src/definitions.ts index 42160dd..68820fb 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -17,6 +17,7 @@ export interface Assets { androidIconForeground?: InputAsset | null; androidIconBackground?: InputAsset | null; + androidBanner?: InputAsset | null; androidSplash?: InputAsset | null; androidSplashDark?: InputAsset | null; androidNotificationIcon?: InputAsset | null; @@ -34,6 +35,7 @@ export const enum AssetKind { IconForeground = 'icon-foreground', IconBackground = 'icon-background', NotificationIcon = 'notification-icon', + Banner = 'banner', Splash = 'splash', SplashDark = 'splash-dark', } @@ -144,6 +146,10 @@ export interface PwaOutputAssetTemplate extends OutputAssetTemplate { export interface AndroidOutputAssetTemplate extends OutputAssetTemplate { density: AndroidDensity; } + +export interface AndroidOutputAssetTemplateBanner extends OutputAssetTemplate { + density: AndroidDensity; +} export interface AndroidOutputAssetTemplateSplash extends OutputAssetTemplate { density: AndroidDensity; orientation: Orientation; diff --git a/src/platforms/android/assets.ts b/src/platforms/android/assets.ts index 37592d6..aff7e94 100644 --- a/src/platforms/android/assets.ts +++ b/src/platforms/android/assets.ts @@ -1,6 +1,7 @@ import type { AndroidOutputAssetTemplate, AndroidOutputAssetTemplateAdaptiveIcon, + AndroidOutputAssetTemplateBanner, AndroidOutputAssetTemplateSplash, } from '../../definitions'; import { AssetKind, AndroidDensity, Format, Orientation, Platform } from '../../definitions'; @@ -116,6 +117,55 @@ export const ANDROID_XXXHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIc density: AndroidDensity.Xxxhdpi, }; +// +// Banners +// + +export const ANDROID_MDPI_BANNER: AndroidOutputAssetTemplateBanner = { + platform: Platform.Android, + kind: AssetKind.Banner, + format: Format.Png, + width: 160, + height: 90, + density: AndroidDensity.Mdpi, +}; + +export const ANDROID_HDPI_BANNER: AndroidOutputAssetTemplateBanner = { + platform: Platform.Android, + kind: AssetKind.Banner, + format: Format.Png, + width: 240, + height: 135, + density: AndroidDensity.Hdpi, +}; + +export const ANDROID_XHDPI_BANNER: AndroidOutputAssetTemplateBanner = { + platform: Platform.Android, + kind: AssetKind.Banner, + format: Format.Png, + width: 320, + height: 180, + density: AndroidDensity.Xhdpi, +}; + +export const ANDROID_XXHDPI_BANNER: AndroidOutputAssetTemplateBanner = { + platform: Platform.Android, + kind: AssetKind.Banner, + format: Format.Png, + width: 480, + height: 270, + density: AndroidDensity.Xxhdpi, +}; + +export const ANDROID_XXXHDPI_BANNER: AndroidOutputAssetTemplateBanner = { + platform: Platform.Android, + kind: AssetKind.Banner, + format: Format.Png, + width: 640, + height: 360, + density: AndroidDensity.Xxxhdpi, +}; + // // Splash screens // diff --git a/src/platforms/android/index.ts b/src/platforms/android/index.ts index f7664bb..12d6c7e 100644 --- a/src/platforms/android/index.ts +++ b/src/platforms/android/index.ts @@ -10,6 +10,7 @@ import type { AndroidOutputAssetTemplate, AndroidOutputAssetTemplateAdaptiveIcon, AndroidOutputAssetTemplateSplash, + AndroidOutputAssetTemplateBanner, } from '../../definitions'; import { AssetKind, Platform } from '../../definitions'; import { BadPipelineError, BadProjectError } from '../../error'; @@ -46,6 +47,8 @@ export class AndroidAssetGenerator extends AssetGenerator { return this.generateAdaptiveIconForeground(asset, project); case AssetKind.IconBackground: return this.generateAdaptiveIconBackground(asset, project); + case AssetKind.Banner: + return this.generateBanners(asset, project); case AssetKind.Splash: case AssetKind.SplashDark: return this.generateSplashes(asset, project); @@ -76,8 +79,24 @@ export class AndroidAssetGenerator extends AssetGenerator { const generatedLegacyIcons = await this.generateLegacyIcon(asset, project); generated.push(...generatedLegacyIcons); - const splashes = Object.values(AndroidAssetTemplates).filter((a) => a.kind === AssetKind.Splash); + // Generate banners + const banners = Object.values(AndroidAssetTemplates).filter((a) => a.kind === AssetKind.Banner); + const generatedBanners = await Promise.all( + banners.map(async (banner) => { + return this._generateBannersFromLogo( + project, + asset, + banner, + pipe, + this.options.splashBackgroundColor ?? '#ffffff', + ); + }), + ); + + generated.push(...generatedBanners); + // Generate splashes + const splashes = Object.values(AndroidAssetTemplates).filter((a) => a.kind === AssetKind.Splash); const generatedSplashes = await Promise.all( splashes.map(async (splash) => { return this._generateSplashesFromLogo( @@ -156,6 +175,70 @@ export class AndroidAssetGenerator extends AssetGenerator { return [...foregroundImages, ...backgroundImages]; } + private async _generateBannersFromLogo( + project: Project, + asset: InputAsset, + splash: AndroidOutputAssetTemplate, + pipe: Sharp, + backgroundColor: string, + ): Promise { + // Generate light splash + const resPath = this.getResPath(project); + + let drawableDir = `drawable`; + if (splash.density) { + drawableDir = `drawable-${splash.density}`; + } + + const parentDir = join(resPath, drawableDir); + if (!(await pathExists(parentDir))) { + await mkdirp(parentDir); + } + const dest = join(resPath, drawableDir, 'banner.png'); + + const targetLogoWidthPercent = this.options.logoSplashScale ?? 0.2; + let targetWidth = this.options.logoSplashTargetWidth ?? Math.floor((splash.width ?? 0) * targetLogoWidthPercent); + + if (targetWidth > splash.width || targetWidth > splash.height) { + targetWidth = Math.floor((splash.width ?? 0) * targetLogoWidthPercent); + } + + if (targetWidth > splash.width || targetWidth > splash.height) { + warn(`Logo dimensions exceed dimensions of splash ${splash.width}x${splash.height}, using default logo size`); + targetWidth = Math.floor((splash.width ?? 0) * 0.2); + } + + const canvas = sharp({ + create: { + width: splash.width ?? 0, + height: splash.height ?? 0, + channels: 4, + background: backgroundColor, + }, + }); + + const resized = await sharp(asset.path).resize(targetWidth).toBuffer(); + + const outputInfo = await canvas + .composite([{ input: resized, gravity: sharp.gravity.center }]) + .png() + .toFile(dest); + + const splashOutput = new OutputAsset( + splash, + asset, + project, + { + [dest]: dest, + }, + { + [dest]: outputInfo, + }, + ); + + return splashOutput; + } + private async _generateSplashesFromLogo( project: Project, asset: InputAsset, @@ -474,12 +557,57 @@ export class AndroidAssetGenerator extends AssetGenerator { private async updateManifest(project: Project) { project.android?.getAndroidManifest()?.setAttrs('manifest/application', { 'android:icon': '@mipmap/ic_launcher', + 'android:banner': '@drawable/banner', 'android:roundIcon': '@mipmap/ic_launcher_round', }); await project.commit(); } + private async generateBanners (asset: InputAsset, project: Project): Promise { + const pipe = asset.pipeline(); + + if (!pipe) { + throw new BadPipelineError('Sharp instance not created'); + } + + const banners = Object.values(AndroidAssetTemplates).filter((a) => a.kind === AssetKind.Banner) as AndroidOutputAssetTemplateBanner[]; + + const resPath = this.getResPath(project); + + const collected = await Promise.all( + banners.map(async (banner) => { + const [dest, outputInfo] = await this.generateBanner(project, asset, banner, pipe); + + const relPath = relative(resPath, dest); + return new OutputAsset(banner, asset, project, { [relPath]: dest }, { [relPath]: outputInfo }); + }), + ); + + return collected; + } + + private async generateBanner( + project: Project, + asset: InputAsset, + template: AndroidOutputAssetTemplateBanner, + pipe: Sharp, + ): Promise<[string, OutputInfo]> { + const drawableDir = template.density ? `drawable-${template.density}` : 'drawable'; + + const resPath = this.getResPath(project); + const parentDir = join(resPath, drawableDir); + if (!(await pathExists(parentDir))) { + await mkdirp(parentDir); + } + const dest = join(resPath, drawableDir, 'banner.png'); + + const outputInfo = await pipe.resize(template.width, template.height).png().toFile(dest); + + return [dest, outputInfo]; + } + + private async generateSplashes(asset: InputAsset, project: Project): Promise { const pipe = asset.pipeline(); diff --git a/src/project.ts b/src/project.ts index 325bf10..7e1ad86 100644 --- a/src/project.ts +++ b/src/project.ts @@ -67,6 +67,7 @@ export class Project extends MobileProject { androidIconForeground: await this.loadInputAsset('android/icon-foreground', AssetKind.Icon, Platform.Android), androidIconBackground: await this.loadInputAsset('android/icon-background', AssetKind.Icon, Platform.Android), + androidBanner: await this.loadInputAsset('android/banner', AssetKind.Banner, Platform.Android), androidSplash: await this.loadInputAsset('android/splash', AssetKind.Splash, Platform.Android), androidSplashDark: await this.loadInputAsset('android/splash-dark', AssetKind.SplashDark, Platform.Android), androidNotificationIcon: await this.loadInputAsset( diff --git a/test/platforms/android.asset.test.ts b/test/platforms/android.asset.test.ts index c21b205..65ad5e3 100644 --- a/test/platforms/android.asset.test.ts +++ b/test/platforms/android.asset.test.ts @@ -175,7 +175,7 @@ describe('Android Asset Test - Logo Only', () => { let generatedAssets = ((await assets.logo?.generate(strategy, ctx.project)) ?? []) as OutputAsset[]; - expect(generatedAssets.length).toBe(50); + expect(generatedAssets.length).toBe(55); await verifySizes(generatedAssets); }); From 09ee7e21673f6ce17c677efe76739e5eb28fe908 Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:44:36 +0200 Subject: [PATCH 2/2] fix: include dist --- .gitignore | 1 - dist/asset-generator.d.ts | 19 ++ dist/asset-generator.js | 9 + dist/colors.d.ts | 23 ++ dist/colors.js | 27 ++ dist/ctx.d.ts | 11 + dist/ctx.js | 52 ++++ dist/definitions.d.ts | 152 +++++++++++ dist/definitions.js | 2 + dist/error.d.ts | 18 ++ dist/error.js | 37 +++ dist/index.d.ts | 3 + dist/index.js | 71 +++++ dist/input-asset.d.ts | 24 ++ dist/input-asset.js | 46 ++++ dist/output-asset.d.ts | 25 ++ dist/output-asset.js | 22 ++ dist/platforms/android/assets.d.ts | 47 ++++ dist/platforms/android/assets.js | 383 ++++++++++++++++++++++++++ dist/platforms/android/index.d.ts | 31 +++ dist/platforms/android/index.js | 419 ++++++++++++++++++++++++++++ dist/platforms/ios/assets.d.ts | 13 + dist/platforms/ios/assets.js | 89 ++++++ dist/platforms/ios/index.d.ts | 21 ++ dist/platforms/ios/index.js | 266 ++++++++++++++++++ dist/platforms/pwa/assets.d.ts | 20 ++ dist/platforms/pwa/assets.js | 93 +++++++ dist/platforms/pwa/index.d.ts | 31 +++ dist/platforms/pwa/index.js | 425 +++++++++++++++++++++++++++++ dist/project.d.ts | 18 ++ dist/project.js | 92 +++++++ dist/tasks/generate.d.ts | 3 + dist/tasks/generate.js | 139 ++++++++++ dist/util/cli.d.ts | 1 + dist/util/cli.js | 31 +++ dist/util/log.d.ts | 8 + dist/util/log.js | 51 ++++ dist/util/subprocess.d.ts | 1 + dist/util/subprocess.js | 22 ++ dist/util/term.d.ts | 2 + dist/util/term.js | 30 ++ package.json | 4 +- 42 files changed, 2779 insertions(+), 3 deletions(-) create mode 100644 dist/asset-generator.d.ts create mode 100644 dist/asset-generator.js create mode 100644 dist/colors.d.ts create mode 100644 dist/colors.js create mode 100644 dist/ctx.d.ts create mode 100644 dist/ctx.js create mode 100644 dist/definitions.d.ts create mode 100644 dist/definitions.js create mode 100644 dist/error.d.ts create mode 100644 dist/error.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/input-asset.d.ts create mode 100644 dist/input-asset.js create mode 100644 dist/output-asset.d.ts create mode 100644 dist/output-asset.js create mode 100644 dist/platforms/android/assets.d.ts create mode 100644 dist/platforms/android/assets.js create mode 100644 dist/platforms/android/index.d.ts create mode 100644 dist/platforms/android/index.js create mode 100644 dist/platforms/ios/assets.d.ts create mode 100644 dist/platforms/ios/assets.js create mode 100644 dist/platforms/ios/index.d.ts create mode 100644 dist/platforms/ios/index.js create mode 100644 dist/platforms/pwa/assets.d.ts create mode 100644 dist/platforms/pwa/assets.js create mode 100644 dist/platforms/pwa/index.d.ts create mode 100644 dist/platforms/pwa/index.js create mode 100644 dist/project.d.ts create mode 100644 dist/project.js create mode 100644 dist/tasks/generate.d.ts create mode 100644 dist/tasks/generate.js create mode 100644 dist/util/cli.d.ts create mode 100644 dist/util/cli.js create mode 100644 dist/util/log.d.ts create mode 100644 dist/util/log.js create mode 100644 dist/util/subprocess.d.ts create mode 100644 dist/util/subprocess.js create mode 100644 dist/util/term.d.ts create mode 100644 dist/util/term.js diff --git a/.gitignore b/.gitignore index 1ea8ab4..d3aeba1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -dist node_modules src-old/ test-old/ diff --git a/dist/asset-generator.d.ts b/dist/asset-generator.d.ts new file mode 100644 index 0000000..888ea27 --- /dev/null +++ b/dist/asset-generator.d.ts @@ -0,0 +1,19 @@ +import type { InputAsset } from './input-asset'; +import type { OutputAsset } from './output-asset'; +import type { Project } from './project'; +export declare abstract class AssetGenerator { + options: AssetGeneratorOptions; + constructor(options: AssetGeneratorOptions); + abstract generate(asset: InputAsset, project: Project): Promise; +} +export interface AssetGeneratorOptions { + iconBackgroundColor?: string; + iconBackgroundColorDark?: string; + splashBackgroundColor?: string; + splashBackgroundColorDark?: string; + pwaManifestPath?: string; + pwaNoAppleFetch?: boolean; + logoSplashScale?: number; + logoSplashTargetWidth?: number; + androidFlavor?: string; +} diff --git a/dist/asset-generator.js b/dist/asset-generator.js new file mode 100644 index 0000000..e2f67b7 --- /dev/null +++ b/dist/asset-generator.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AssetGenerator = void 0; +class AssetGenerator { + constructor(options) { + this.options = options; + } +} +exports.AssetGenerator = AssetGenerator; diff --git a/dist/colors.d.ts b/dist/colors.d.ts new file mode 100644 index 0000000..10ca64e --- /dev/null +++ b/dist/colors.d.ts @@ -0,0 +1,23 @@ +import kleur from 'kleur'; +export declare const strong: kleur.Color; +export declare const weak: kleur.Color; +export declare const input: kleur.Color; +export declare const success: kleur.Color; +export declare const failure: kleur.Color; +export declare const ancillary: kleur.Color; +export declare const extra: kleur.Color; +declare const COLORS: { + strong: kleur.Color; + weak: kleur.Color; + input: kleur.Color; + success: kleur.Color; + failure: kleur.Color; + ancillary: kleur.Color; + log: { + DEBUG: kleur.Color; + INFO: kleur.Color; + WARN: kleur.Color; + ERROR: kleur.Color; + }; +}; +export default COLORS; diff --git a/dist/colors.js b/dist/colors.js new file mode 100644 index 0000000..d412d0f --- /dev/null +++ b/dist/colors.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extra = exports.ancillary = exports.failure = exports.success = exports.input = exports.weak = exports.strong = void 0; +const tslib_1 = require("tslib"); +const kleur_1 = (0, tslib_1.__importDefault)(require("kleur")); +exports.strong = kleur_1.default.bold; +exports.weak = kleur_1.default.dim; +exports.input = kleur_1.default.cyan; +exports.success = kleur_1.default.green; +exports.failure = kleur_1.default.red; +exports.ancillary = kleur_1.default.cyan; +exports.extra = kleur_1.default.yellow; +const COLORS = { + strong: exports.strong, + weak: exports.weak, + input: exports.input, + success: exports.success, + failure: exports.failure, + ancillary: exports.ancillary, + log: { + DEBUG: kleur_1.default.magenta, + INFO: kleur_1.default.cyan, + WARN: kleur_1.default.yellow, + ERROR: kleur_1.default.red, + }, +}; +exports.default = COLORS; diff --git a/dist/ctx.d.ts b/dist/ctx.d.ts new file mode 100644 index 0000000..1252c1d --- /dev/null +++ b/dist/ctx.d.ts @@ -0,0 +1,11 @@ +import type { AssetGeneratorOptions } from './asset-generator'; +import { Project } from './project'; +export interface Context { + projectRootPath?: string; + args: AssetGeneratorOptions | any; + project: Project; + nodePackageRoot: string; + rootDir: string; +} +export declare function loadContext(projectRootPath?: string): Promise; +export declare function setArguments(ctx: Context, args: any): void; diff --git a/dist/ctx.js b/dist/ctx.js new file mode 100644 index 0000000..1128cee --- /dev/null +++ b/dist/ctx.js @@ -0,0 +1,52 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setArguments = exports.loadContext = void 0; +const tslib_1 = require("tslib"); +const path_1 = require("path"); +const yargs_1 = (0, tslib_1.__importDefault)(require("yargs")); +const helpers_1 = require("yargs/helpers"); +const project_1 = require("./project"); +async function loadContext(projectRootPath) { + var _a; + const rootDir = process.cwd(); + const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)).argv; + let project; + try { + project = await loadProject(argv, projectRootPath, (_a = argv.assetPath) !== null && _a !== void 0 ? _a : 'assets'); + } + catch (e) { + throw new Error(`Unable to load project: ${e.message}`); + } + return { + args: argv, + project, + projectRootPath, + // Important for resolving custom prettier plugin + nodePackageRoot: (0, path_1.join)(__dirname, '../../'), + rootDir, + }; +} +exports.loadContext = loadContext; +function setArguments(ctx, args) { + ctx.args = args; + process.env.VERBOSE = '' + !!args.verbose; +} +exports.setArguments = setArguments; +async function loadProject(args, projectRootPath, projectAssetPath) { + const config = await loadMobileProjectConfig(args); + const project = new project_1.Project(projectRootPath, config, projectAssetPath); + await project.load(); + return project; +} +// TODO: Use the config loading stuff from @capacitor/configure +function loadMobileProjectConfig(args) { + var _a, _b; + return { + ios: { + path: (_a = args.iosProject) !== null && _a !== void 0 ? _a : 'ios/App', + }, + android: { + path: (_b = args.androidProject) !== null && _b !== void 0 ? _b : 'android', + }, + }; +} diff --git a/dist/definitions.d.ts b/dist/definitions.d.ts new file mode 100644 index 0000000..d9c3d9e --- /dev/null +++ b/dist/definitions.d.ts @@ -0,0 +1,152 @@ +import type { InputAsset } from './input-asset'; +export interface Assets { + logo: InputAsset | null; + logoDark: InputAsset | null; + icon: InputAsset | null; + iconForeground: InputAsset | null; + iconBackground: InputAsset | null; + splash: InputAsset | null; + splashDark: InputAsset | null; + iosIcon?: InputAsset | null; + iosSplash?: InputAsset | null; + iosSplashDark?: InputAsset | null; + androidIcon?: InputAsset | null; + androidIconForeground?: InputAsset | null; + androidIconBackground?: InputAsset | null; + androidBanner?: InputAsset | null; + androidSplash?: InputAsset | null; + androidSplashDark?: InputAsset | null; + androidNotificationIcon?: InputAsset | null; + pwaIcon?: InputAsset | null; + pwaSplash?: InputAsset | null; + pwaSplashDark?: InputAsset | null; +} +export declare const enum AssetKind { + Logo = "logo", + LogoDark = "logo-dark", + AdaptiveIcon = "adaptive-icon", + Icon = "icon", + IconForeground = "icon-foreground", + IconBackground = "icon-background", + NotificationIcon = "notification-icon", + Banner = "banner", + Splash = "splash", + SplashDark = "splash-dark" +} +export declare const enum Platform { + Any = "any", + Ios = "ios", + Android = "android", + Pwa = "pwa" +} +export declare const enum Format { + Png = "png", + Jpeg = "jpeg", + Svg = "svg", + WebP = "webp", + Unknown = "unknown" +} +export declare const enum Orientation { + Default = "", + Portrait = "portrait", + Landscape = "landscape" +} +export declare const enum Theme { + Any = "any", + Light = "light", + Dark = "dark" +} +export declare const enum AndroidDensity { + Default = "", + Ldpi = "ldpi", + Mdpi = "mdpi", + Hdpi = "hdpi", + Xhdpi = "xhdpi", + Xxhdpi = "xxhdpi", + Xxxhdpi = "xxxhdpi", + LandLdpi = "land-ldpi", + LandMdpi = "land-mdpi", + LandHdpi = "land-hdpi", + LandXhdpi = "land-xhdpi", + LandXxhdpi = "land-xxhdpi", + LandXxxhdpi = "land-xxxhdpi", + PortLdpi = "port-ldpi", + PortMdpi = "port-mdpi", + PortHdpi = "port-hdpi", + PortXhdpi = "port-xhdpi", + PortXxhdpi = "port-xxhdpi", + PortXxxhdpi = "port-xxxhdpi", + DefaultNight = "night", + LdpiNight = "night-ldpi", + MdpiNight = "night-mdpi", + HdpiNight = "night-hdpi", + XhdpiNight = "night-xhdpi", + XxhdpiNight = "night-xxhdpi", + XxxhdpiNight = "night-xxxhdpi", + LandLdpiNight = "land-night-ldpi", + LandMdpiNight = "land-night-mdpi", + LandHdpiNight = "land-night-hdpi", + LandXhdpiNight = "land-night-xhdpi", + LandXxhdpiNight = "land-night-xxhdpi", + LandXxxhdpiNight = "land-night-xxxhdpi", + PortLdpiNight = "port-night-ldpi", + PortMdpiNight = "port-night-mdpi", + PortHdpiNight = "port-night-hdpi", + PortXhdpiNight = "port-night-xhdpi", + PortXxhdpiNight = "port-night-xxhdpi", + PortXxxhdpiNight = "port-night-xxxhdpi" +} +export interface OutputAssetTemplate { + platform: Platform; + kind: AssetKind; + format: Format; + width: number; + height: number; + scale?: number; +} +export interface IosOutputAssetTemplate extends OutputAssetTemplate { + name: string; + idiom: IosIdiom; +} +export declare const enum IosIdiom { + Universal = "universal", + iPhone = "iphone", + iPad = "ipad", + Watch = "watch", + TV = "tv" +} +export declare type IosOutputAssetTemplateIcon = IosOutputAssetTemplate; +export interface IosOutputAssetTemplateSplash extends IosOutputAssetTemplate { + orientation: Orientation; + theme: Theme; +} +export interface PwaOutputAssetTemplate extends OutputAssetTemplate { + name: string; + orientation?: Orientation; + density?: string; +} +export interface AndroidOutputAssetTemplate extends OutputAssetTemplate { + density: AndroidDensity; +} +export interface AndroidOutputAssetTemplateBanner extends OutputAssetTemplate { + density: AndroidDensity; +} +export interface AndroidOutputAssetTemplateSplash extends OutputAssetTemplate { + density: AndroidDensity; + orientation: Orientation; +} +export interface AndroidOutputAssetTemplateAdaptiveIcon extends OutputAssetTemplate { + density: AndroidDensity; +} +export interface IosContents { + images: { + filename: string; + size: string; + scale: string; + idiom: string; + }[]; + info?: { + version: number; + author: string; + }; +} diff --git a/dist/definitions.js b/dist/definitions.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/dist/definitions.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/error.d.ts b/dist/error.d.ts new file mode 100644 index 0000000..24152df --- /dev/null +++ b/dist/error.d.ts @@ -0,0 +1,18 @@ +export declare abstract class BaseError extends Error { + readonly message: string; + abstract readonly name: string; + abstract readonly code: string; + constructor(message: string); + toString(): string; + toJSON(): { + [key: string]: any; + }; +} +export declare class BadProjectError extends BaseError { + readonly name = "BadProjectError"; + readonly code = "BAD_PROJECT"; +} +export declare class BadPipelineError extends BaseError { + readonly name = "BadPipelineError"; + readonly code = "BAD_SHARP_PIPELINE"; +} diff --git a/dist/error.js b/dist/error.js new file mode 100644 index 0000000..59db077 --- /dev/null +++ b/dist/error.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BadPipelineError = exports.BadProjectError = exports.BaseError = void 0; +class BaseError extends Error { + constructor(message) { + super(message); + this.message = message; + this.stack = new Error().stack || ''; + this.message = message; + } + toString() { + return this.message; + } + toJSON() { + return { + code: this.code, + message: this.message, + }; + } +} +exports.BaseError = BaseError; +class BadProjectError extends BaseError { + constructor() { + super(...arguments); + this.name = 'BadProjectError'; + this.code = 'BAD_PROJECT'; + } +} +exports.BadProjectError = BadProjectError; +class BadPipelineError extends BaseError { + constructor() { + super(...arguments); + this.name = 'BadPipelineError'; + this.code = 'BAD_SHARP_PIPELINE'; + } +} +exports.BadPipelineError = BadPipelineError; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..84e6930 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,3 @@ +import type { Context } from './ctx'; +export declare function run(): Promise; +export declare function runProgram(ctx: Context): void; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..7d63dc7 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,71 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runProgram = exports.run = void 0; +const tslib_1 = require("tslib"); +const commander_1 = require("commander"); +const c = (0, tslib_1.__importStar)(require("./colors")); +const ctx_1 = require("./ctx"); +const cli_1 = require("./util/cli"); +const log_1 = require("./util/log"); +async function run() { + try { + const ctx = await (0, ctx_1.loadContext)(); + runProgram(ctx); + } + catch (e) { + process.exitCode = 1; + log_1.logger.error(e.message ? e.message : String(e)); + throw e; + } +} +exports.run = run; +function parseIntOption(value) { + // parseInt takes a string and a radix + const parsedValue = parseInt(value, 10); + if (isNaN(parsedValue)) { + throw new commander_1.InvalidArgumentError('Not a number.'); + } + return parsedValue; +} +function runProgram(ctx) { + // program.version(env.package.version); + const program = new commander_1.Command(); + program + .command('generate') + .description(`Run image generation`) + .option('--verbose', 'Verbose output') + .option('--ios', 'Generate iOS assets') + .option('--android', 'Generate Android assets') + .option('--pwa', 'Generate PWA/Web assets') + .option('--logoSplashScale ', 'Scaling factor for logo when generating splash screens from a single logo file') + .option('--logoSplashTargetWidth ', 'A specific target width for logo to use when generating splash screens from a single logo file', parseIntOption) + .option('--iconBackgroundColor ', 'Background color used for icons when generating from a single logo file') + .option('--iconBackgroundColorDark ', 'Background color used for icon in dark mode when generating from a single logo file') + .option('--splashBackgroundColor ', 'Background color used for splash screens when generating from a single logo file') + .option('--splashBackgroundColorDark ', 'Background color used for splash screens in dark mode when generating from a single logo file') + .option('--pwaManifestPath ', "Path to the web app's manifest.json or manifest.webmanifest file") + .option('--pwaNoAppleFetch', 'Whether to fetch the latest screen sizes for Apple devices from the official Apple site. Set to true if running offline to use local cached sizes (may be occasionally out of date)') + .option('--assetPath ', 'Path to the assets directory for your project. By default will check "assets" and "resources" directories, in that order.') + .option('--androidFlavor ', 'Android product flavor name where generated assets will be created. Defaults to "main".') + .option('--iosProject ', 'Path to iOS project (defaults to "ios/App")') + .option('--androidProject ', 'Path to Android project (defaults to "android")') + /* + .option( + '--pwaTags', + 'Log tags necessary for including generated PWA assets in your index.html file', + ) + */ + .action((0, cli_1.wrapAction)(async (args = {}) => { + (0, ctx_1.setArguments)(ctx, args); + const { run } = await Promise.resolve().then(() => (0, tslib_1.__importStar)(require('./tasks/generate'))); + await run(ctx); + })); + program.arguments('[command]').action( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (0, cli_1.wrapAction)((_) => { + (0, log_1.log)(c.strong('\n⚡️ Capacitor Assets ⚡️\n')); + program.outputHelp(); + })); + program.parse(process.argv); +} +exports.runProgram = runProgram; diff --git a/dist/input-asset.d.ts b/dist/input-asset.d.ts new file mode 100644 index 0000000..00b6fb4 --- /dev/null +++ b/dist/input-asset.d.ts @@ -0,0 +1,24 @@ +import sharp from 'sharp'; +import type { AssetGenerator } from './asset-generator'; +import type { AssetKind, Platform } from './definitions'; +import { Format } from './definitions'; +import type { OutputAsset } from './output-asset'; +import type { Project } from './project'; +/** + * An instance of an asset that we will use to generate + * a number of output assets. + */ +export declare class InputAsset { + path: string; + kind: AssetKind; + platform: Platform; + private filename; + width?: number; + height?: number; + private _sharp; + constructor(path: string, kind: AssetKind, platform: Platform); + pipeline(): sharp.Sharp | undefined; + format(): Format.Jpeg | Format.Png | Format.Svg | Format.Unknown; + load(): Promise; + generate(strategy: AssetGenerator, project: Project): Promise; +} diff --git a/dist/input-asset.js b/dist/input-asset.js new file mode 100644 index 0000000..0e670c6 --- /dev/null +++ b/dist/input-asset.js @@ -0,0 +1,46 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.InputAsset = void 0; +const tslib_1 = require("tslib"); +const path_1 = require("path"); +const sharp_1 = (0, tslib_1.__importDefault)(require("sharp")); +/** + * An instance of an asset that we will use to generate + * a number of output assets. + */ +class InputAsset { + constructor(path, kind, platform) { + this.path = path; + this.kind = kind; + this.platform = platform; + this._sharp = null; + this.filename = (0, path_1.basename)(path); + } + pipeline() { + var _a; + return (_a = this._sharp) === null || _a === void 0 ? void 0 : _a.clone(); + } + format() { + const ext = (0, path_1.extname)(this.filename); + switch (ext) { + case '.png': + return "png" /* Png */; + case '.jpg': + case '.jpeg': + return "jpeg" /* Jpeg */; + case '.svg': + return "svg" /* Svg */; + } + return "unknown" /* Unknown */; + } + async load() { + this._sharp = await (0, sharp_1.default)(this.path); + const metadata = await this._sharp.metadata(); + this.width = metadata.width; + this.height = metadata.height; + } + async generate(strategy, project) { + return strategy.generate(this, project); + } +} +exports.InputAsset = InputAsset; diff --git a/dist/output-asset.d.ts b/dist/output-asset.d.ts new file mode 100644 index 0000000..50c622b --- /dev/null +++ b/dist/output-asset.d.ts @@ -0,0 +1,25 @@ +import type { OutputInfo } from 'sharp'; +import type { OutputAssetTemplate } from './definitions'; +import type { InputAsset } from './input-asset'; +import type { Project } from './project'; +/** + * An instance of a generated asset + */ +export declare class OutputAsset { + template: OutputAssetTemplateType; + asset: InputAsset; + project: Project; + destFilenames: { + [name: string]: string; + }; + outputInfoMap: { + [name: string]: OutputInfo; + }; + constructor(template: OutputAssetTemplateType, asset: InputAsset, project: Project, destFilenames: { + [name: string]: string; + }, outputInfoMap: { + [name: string]: OutputInfo; + }); + getDestFilename(assetName: string): string; + getOutputInfo(assetName: string): OutputInfo; +} diff --git a/dist/output-asset.js b/dist/output-asset.js new file mode 100644 index 0000000..bb74339 --- /dev/null +++ b/dist/output-asset.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OutputAsset = void 0; +/** + * An instance of a generated asset + */ +class OutputAsset { + constructor(template, asset, project, destFilenames, outputInfoMap) { + this.template = template; + this.asset = asset; + this.project = project; + this.destFilenames = destFilenames; + this.outputInfoMap = outputInfoMap; + } + getDestFilename(assetName) { + return this.destFilenames[assetName]; + } + getOutputInfo(assetName) { + return this.outputInfoMap[assetName]; + } +} +exports.OutputAsset = OutputAsset; diff --git a/dist/platforms/android/assets.d.ts b/dist/platforms/android/assets.d.ts new file mode 100644 index 0000000..10ed0f0 --- /dev/null +++ b/dist/platforms/android/assets.d.ts @@ -0,0 +1,47 @@ +import type { AndroidOutputAssetTemplate, AndroidOutputAssetTemplateAdaptiveIcon, AndroidOutputAssetTemplateBanner, AndroidOutputAssetTemplateSplash } from '../../definitions'; +export declare const ANDROID_LDPI_ICON: AndroidOutputAssetTemplate; +export declare const ANDROID_MDPI_ICON: AndroidOutputAssetTemplate; +export declare const ANDROID_HDPI_ICON: AndroidOutputAssetTemplate; +export declare const ANDROID_XHDPI_ICON: AndroidOutputAssetTemplate; +export declare const ANDROID_XXHDPI_ICON: AndroidOutputAssetTemplate; +export declare const ANDROID_XXXHDPI_ICON: AndroidOutputAssetTemplate; +/** + * Adaptive icons + */ +export declare const ANDROID_LDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon; +export declare const ANDROID_MDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon; +export declare const ANDROID_HDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon; +export declare const ANDROID_XHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon; +export declare const ANDROID_XXHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon; +export declare const ANDROID_XXXHDPI_ADAPTIVE_ICON: AndroidOutputAssetTemplateAdaptiveIcon; +export declare const ANDROID_MDPI_BANNER: AndroidOutputAssetTemplateBanner; +export declare const ANDROID_HDPI_BANNER: AndroidOutputAssetTemplateBanner; +export declare const ANDROID_XHDPI_BANNER: AndroidOutputAssetTemplateBanner; +export declare const ANDROID_XXHDPI_BANNER: AndroidOutputAssetTemplateBanner; +export declare const ANDROID_XXXHDPI_BANNER: AndroidOutputAssetTemplateBanner; +export declare const ANDROID_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_LDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_MDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_HDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_XHDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_XXHDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_XXXHDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_LDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_MDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_HDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_XHDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_XXHDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_XXXHDPI_SCREEN: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_LDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_MDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_HDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_XHDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_XXHDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_LAND_XXXHDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_LDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_MDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_HDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_XHDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_XXHDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; +export declare const ANDROID_PORT_XXXHDPI_SCREEN_DARK: AndroidOutputAssetTemplateSplash; diff --git a/dist/platforms/android/assets.js b/dist/platforms/android/assets.js new file mode 100644 index 0000000..07969d4 --- /dev/null +++ b/dist/platforms/android/assets.js @@ -0,0 +1,383 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ANDROID_PORT_XXXHDPI_SCREEN_DARK = exports.ANDROID_PORT_XXHDPI_SCREEN_DARK = exports.ANDROID_PORT_XHDPI_SCREEN_DARK = exports.ANDROID_PORT_HDPI_SCREEN_DARK = exports.ANDROID_PORT_MDPI_SCREEN_DARK = exports.ANDROID_PORT_LDPI_SCREEN_DARK = exports.ANDROID_LAND_XXXHDPI_SCREEN_DARK = exports.ANDROID_LAND_XXHDPI_SCREEN_DARK = exports.ANDROID_LAND_XHDPI_SCREEN_DARK = exports.ANDROID_LAND_HDPI_SCREEN_DARK = exports.ANDROID_LAND_MDPI_SCREEN_DARK = exports.ANDROID_LAND_LDPI_SCREEN_DARK = exports.ANDROID_SCREEN_DARK = exports.ANDROID_PORT_XXXHDPI_SCREEN = exports.ANDROID_PORT_XXHDPI_SCREEN = exports.ANDROID_PORT_XHDPI_SCREEN = exports.ANDROID_PORT_HDPI_SCREEN = exports.ANDROID_PORT_MDPI_SCREEN = exports.ANDROID_PORT_LDPI_SCREEN = exports.ANDROID_LAND_XXXHDPI_SCREEN = exports.ANDROID_LAND_XXHDPI_SCREEN = exports.ANDROID_LAND_XHDPI_SCREEN = exports.ANDROID_LAND_HDPI_SCREEN = exports.ANDROID_LAND_MDPI_SCREEN = exports.ANDROID_LAND_LDPI_SCREEN = exports.ANDROID_SCREEN = exports.ANDROID_XXXHDPI_BANNER = exports.ANDROID_XXHDPI_BANNER = exports.ANDROID_XHDPI_BANNER = exports.ANDROID_HDPI_BANNER = exports.ANDROID_MDPI_BANNER = exports.ANDROID_XXXHDPI_ADAPTIVE_ICON = exports.ANDROID_XXHDPI_ADAPTIVE_ICON = exports.ANDROID_XHDPI_ADAPTIVE_ICON = exports.ANDROID_HDPI_ADAPTIVE_ICON = exports.ANDROID_MDPI_ADAPTIVE_ICON = exports.ANDROID_LDPI_ADAPTIVE_ICON = exports.ANDROID_XXXHDPI_ICON = exports.ANDROID_XXHDPI_ICON = exports.ANDROID_XHDPI_ICON = exports.ANDROID_HDPI_ICON = exports.ANDROID_MDPI_ICON = exports.ANDROID_LDPI_ICON = void 0; +exports.ANDROID_LDPI_ICON = { + platform: "android" /* Android */, + kind: "icon" /* Icon */, + format: "png" /* Png */, + width: 36, + height: 36, + density: "ldpi" /* Ldpi */, +}; +exports.ANDROID_MDPI_ICON = { + platform: "android" /* Android */, + kind: "icon" /* Icon */, + format: "png" /* Png */, + width: 48, + height: 48, + density: "mdpi" /* Mdpi */, +}; +exports.ANDROID_HDPI_ICON = { + platform: "android" /* Android */, + kind: "icon" /* Icon */, + format: "png" /* Png */, + width: 72, + height: 72, + density: "hdpi" /* Hdpi */, +}; +exports.ANDROID_XHDPI_ICON = { + platform: "android" /* Android */, + kind: "icon" /* Icon */, + format: "png" /* Png */, + width: 96, + height: 96, + density: "xhdpi" /* Xhdpi */, +}; +exports.ANDROID_XXHDPI_ICON = { + platform: "android" /* Android */, + kind: "icon" /* Icon */, + format: "png" /* Png */, + width: 144, + height: 144, + density: "xxhdpi" /* Xxhdpi */, +}; +exports.ANDROID_XXXHDPI_ICON = { + platform: "android" /* Android */, + kind: "icon" /* Icon */, + format: "png" /* Png */, + width: 192, + height: 192, + density: "xxxhdpi" /* Xxxhdpi */, +}; +/** + * Adaptive icons + */ +exports.ANDROID_LDPI_ADAPTIVE_ICON = { + platform: "android" /* Android */, + kind: "adaptive-icon" /* AdaptiveIcon */, + format: "png" /* Png */, + width: 81, + height: 81, + density: "ldpi" /* Ldpi */, +}; +exports.ANDROID_MDPI_ADAPTIVE_ICON = { + platform: "android" /* Android */, + kind: "adaptive-icon" /* AdaptiveIcon */, + format: "png" /* Png */, + width: 108, + height: 108, + density: "mdpi" /* Mdpi */, +}; +exports.ANDROID_HDPI_ADAPTIVE_ICON = { + platform: "android" /* Android */, + kind: "adaptive-icon" /* AdaptiveIcon */, + format: "png" /* Png */, + width: 162, + height: 162, + density: "hdpi" /* Hdpi */, +}; +exports.ANDROID_XHDPI_ADAPTIVE_ICON = { + platform: "android" /* Android */, + kind: "adaptive-icon" /* AdaptiveIcon */, + format: "png" /* Png */, + width: 216, + height: 216, + density: "xhdpi" /* Xhdpi */, +}; +exports.ANDROID_XXHDPI_ADAPTIVE_ICON = { + platform: "android" /* Android */, + kind: "adaptive-icon" /* AdaptiveIcon */, + format: "png" /* Png */, + width: 324, + height: 324, + density: "xxhdpi" /* Xxhdpi */, +}; +exports.ANDROID_XXXHDPI_ADAPTIVE_ICON = { + platform: "android" /* Android */, + kind: "adaptive-icon" /* AdaptiveIcon */, + format: "png" /* Png */, + width: 432, + height: 432, + density: "xxxhdpi" /* Xxxhdpi */, +}; +// +// Banners +// +exports.ANDROID_MDPI_BANNER = { + platform: "android" /* Android */, + kind: "banner" /* Banner */, + format: "png" /* Png */, + width: 160, + height: 90, + density: "mdpi" /* Mdpi */, +}; +exports.ANDROID_HDPI_BANNER = { + platform: "android" /* Android */, + kind: "banner" /* Banner */, + format: "png" /* Png */, + width: 240, + height: 135, + density: "hdpi" /* Hdpi */, +}; +exports.ANDROID_XHDPI_BANNER = { + platform: "android" /* Android */, + kind: "banner" /* Banner */, + format: "png" /* Png */, + width: 320, + height: 180, + density: "xhdpi" /* Xhdpi */, +}; +exports.ANDROID_XXHDPI_BANNER = { + platform: "android" /* Android */, + kind: "banner" /* Banner */, + format: "png" /* Png */, + width: 480, + height: 270, + density: "xxhdpi" /* Xxhdpi */, +}; +exports.ANDROID_XXXHDPI_BANNER = { + platform: "android" /* Android */, + kind: "banner" /* Banner */, + format: "png" /* Png */, + width: 640, + height: 360, + density: "xxxhdpi" /* Xxxhdpi */, +}; +// +// Splash screens +// +exports.ANDROID_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 320, + height: 480, + density: "" /* Default */, + orientation: "" /* Default */, +}; +exports.ANDROID_LAND_LDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 320, + height: 240, + density: "land-ldpi" /* LandLdpi */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_MDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 480, + height: 320, + density: "land-mdpi" /* LandMdpi */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_HDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 800, + height: 480, + density: "land-hdpi" /* LandHdpi */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_XHDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 1280, + height: 720, + density: "land-xhdpi" /* LandXhdpi */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_XXHDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 1600, + height: 960, + density: "land-xxhdpi" /* LandXxhdpi */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_XXXHDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 1920, + height: 1280, + density: "land-xxxhdpi" /* LandXxxhdpi */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_PORT_LDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 240, + height: 320, + density: "port-ldpi" /* PortLdpi */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_MDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 320, + height: 480, + density: "port-mdpi" /* PortMdpi */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_HDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 480, + height: 800, + density: "port-hdpi" /* PortHdpi */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_XHDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 720, + height: 1280, + density: "port-xhdpi" /* PortXhdpi */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_XXHDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 960, + height: 1600, + density: "port-xxhdpi" /* PortXxhdpi */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_XXXHDPI_SCREEN = { + platform: "android" /* Android */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + width: 1280, + height: 1920, + density: "port-xxxhdpi" /* PortXxxhdpi */, + orientation: "portrait" /* Portrait */, +}; +// Dark/night mode splashes +exports.ANDROID_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 320, + height: 240, + density: "night" /* DefaultNight */, + orientation: "" /* Default */, +}; +exports.ANDROID_LAND_LDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 320, + height: 240, + density: "land-night-ldpi" /* LandLdpiNight */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_MDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 480, + height: 320, + density: "land-night-mdpi" /* LandMdpiNight */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_HDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 800, + height: 480, + density: "land-night-hdpi" /* LandHdpiNight */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_XHDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 1280, + height: 720, + density: "land-night-xhdpi" /* LandXhdpiNight */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_XXHDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 1600, + height: 960, + density: "land-night-xxhdpi" /* LandXxhdpiNight */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_LAND_XXXHDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 1920, + height: 1280, + density: "land-night-xxxhdpi" /* LandXxxhdpiNight */, + orientation: "landscape" /* Landscape */, +}; +exports.ANDROID_PORT_LDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 240, + height: 320, + density: "port-night-ldpi" /* PortLdpiNight */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_MDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 320, + height: 480, + density: "port-night-mdpi" /* PortMdpiNight */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_HDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 480, + height: 800, + density: "port-night-hdpi" /* PortHdpiNight */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_XHDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 720, + height: 1280, + density: "port-night-xhdpi" /* PortXhdpiNight */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_XXHDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 960, + height: 1600, + density: "port-night-xxhdpi" /* PortXxhdpiNight */, + orientation: "portrait" /* Portrait */, +}; +exports.ANDROID_PORT_XXXHDPI_SCREEN_DARK = { + platform: "android" /* Android */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + width: 1280, + height: 1920, + density: "port-night-xxxhdpi" /* PortXxxhdpiNight */, + orientation: "portrait" /* Portrait */, +}; diff --git a/dist/platforms/android/index.d.ts b/dist/platforms/android/index.d.ts new file mode 100644 index 0000000..a6d332f --- /dev/null +++ b/dist/platforms/android/index.d.ts @@ -0,0 +1,31 @@ +import type { AssetGeneratorOptions } from '../../asset-generator'; +import { AssetGenerator } from '../../asset-generator'; +import type { InputAsset } from '../../input-asset'; +import { OutputAsset } from '../../output-asset'; +import type { Project } from '../../project'; +export declare class AndroidAssetGenerator extends AssetGenerator { + constructor(options?: AssetGeneratorOptions); + generate(asset: InputAsset, project: Project): Promise; + /** + * Generate from logo combines all of the other operations into a single operation + * from a single asset source file. In this mode, a logo along with a background color + * is used to generate all icons and splash screens (with dark mode where possible). + */ + private generateFromLogo; + private _generateAdaptiveIconsFromLogo; + private _generateBannersFromLogo; + private _generateSplashesFromLogo; + private generateLegacyIcon; + private generateLegacyLauncherIcon; + private generateRoundLauncherIcon; + private generateAdaptiveIconForeground; + private _generateAdaptiveIconForeground; + private generateAdaptiveIconBackground; + private _generateAdaptiveIconBackground; + private updateManifest; + private generateBanners; + private generateBanner; + private generateSplashes; + private generateSplash; + private getResPath; +} diff --git a/dist/platforms/android/index.js b/dist/platforms/android/index.js new file mode 100644 index 0000000..baf1cc8 --- /dev/null +++ b/dist/platforms/android/index.js @@ -0,0 +1,419 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AndroidAssetGenerator = void 0; +const tslib_1 = require("tslib"); +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +const utils_fs_1 = require("@ionic/utils-fs"); +const path_1 = require("path"); +const sharp_1 = (0, tslib_1.__importDefault)(require("sharp")); +const asset_generator_1 = require("../../asset-generator"); +const error_1 = require("../../error"); +const output_asset_1 = require("../../output-asset"); +const log_1 = require("../../util/log"); +const AndroidAssetTemplates = (0, tslib_1.__importStar)(require("./assets")); +class AndroidAssetGenerator extends asset_generator_1.AssetGenerator { + constructor(options = {}) { + super(options); + } + async generate(asset, project) { + var _a; + const androidDir = (_a = project.config.android) === null || _a === void 0 ? void 0 : _a.path; + if (!androidDir) { + throw new error_1.BadProjectError('No android project found'); + } + if (asset.platform !== "any" /* Any */ && asset.platform !== "android" /* Android */) { + return []; + } + switch (asset.kind) { + case "logo" /* Logo */: + case "logo-dark" /* LogoDark */: + return this.generateFromLogo(asset, project); + case "icon" /* Icon */: + return this.generateLegacyIcon(asset, project); + case "icon-foreground" /* IconForeground */: + return this.generateAdaptiveIconForeground(asset, project); + case "icon-background" /* IconBackground */: + return this.generateAdaptiveIconBackground(asset, project); + case "banner" /* Banner */: + return this.generateBanners(asset, project); + case "splash" /* Splash */: + case "splash-dark" /* SplashDark */: + return this.generateSplashes(asset, project); + } + return []; + } + /** + * Generate from logo combines all of the other operations into a single operation + * from a single asset source file. In this mode, a logo along with a background color + * is used to generate all icons and splash screens (with dark mode where possible). + */ + async generateFromLogo(asset, project) { + const pipe = asset.pipeline(); + const generated = []; + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + // Generate adaptive icons + const generatedAdaptiveIcons = await this._generateAdaptiveIconsFromLogo(project, asset, pipe); + generated.push(...generatedAdaptiveIcons); + if (asset.kind === "logo" /* Logo */) { + // Generate legacy icons + const generatedLegacyIcons = await this.generateLegacyIcon(asset, project); + generated.push(...generatedLegacyIcons); + // Generate banners + const banners = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "banner" /* Banner */); + const generatedBanners = await Promise.all(banners.map(async (banner) => { + var _a; + return this._generateBannersFromLogo(project, asset, banner, pipe, (_a = this.options.splashBackgroundColor) !== null && _a !== void 0 ? _a : '#ffffff'); + })); + generated.push(...generatedBanners); + // Generate splashes + const splashes = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash" /* Splash */); + const generatedSplashes = await Promise.all(splashes.map(async (splash) => { + var _a; + return this._generateSplashesFromLogo(project, asset, splash, pipe, (_a = this.options.splashBackgroundColor) !== null && _a !== void 0 ? _a : '#ffffff'); + })); + generated.push(...generatedSplashes); + } + // Generate dark splashes + const darkSplashes = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash-dark" /* SplashDark */); + const generatedSplashes = await Promise.all(darkSplashes.map(async (splash) => { + var _a; + return this._generateSplashesFromLogo(project, asset, splash, pipe, (_a = this.options.splashBackgroundColorDark) !== null && _a !== void 0 ? _a : '#111111'); + })); + generated.push(...generatedSplashes); + return [...generated]; + } + // Generate adaptive icons from the source logo + async _generateAdaptiveIconsFromLogo(project, asset, pipe) { + var _a, _b; + // Current versions of Android don't appear to support night mode icons (13+ might?) + // so, for now, we only generate light mode ones + if (asset.kind === "logo-dark" /* LogoDark */) { + return []; + } + // Create the background pipeline for the generated icons + const backgroundPipe = (0, sharp_1.default)({ + create: { + width: asset.width, + height: asset.height, + channels: 4, + background: asset.kind === "logo" /* Logo */ + ? (_a = this.options.iconBackgroundColor) !== null && _a !== void 0 ? _a : '#ffffff' + : (_b = this.options.iconBackgroundColorDark) !== null && _b !== void 0 ? _b : '#111111', + }, + }); + const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "adaptive-icon" /* AdaptiveIcon */); + const backgroundImages = await Promise.all(icons.map(async (icon) => { + return await this._generateAdaptiveIconBackground(project, asset, icon, backgroundPipe); + })); + const foregroundImages = await Promise.all(icons.map(async (icon) => { + return await this._generateAdaptiveIconForeground(project, asset, icon, pipe); + })); + return [...foregroundImages, ...backgroundImages]; + } + async _generateBannersFromLogo(project, asset, splash, pipe, backgroundColor) { + var _a, _b, _c, _d, _e, _f, _g; + // Generate light splash + const resPath = this.getResPath(project); + let drawableDir = `drawable`; + if (splash.density) { + drawableDir = `drawable-${splash.density}`; + } + const parentDir = (0, path_1.join)(resPath, drawableDir); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const dest = (0, path_1.join)(resPath, drawableDir, 'banner.png'); + const targetLogoWidthPercent = (_a = this.options.logoSplashScale) !== null && _a !== void 0 ? _a : 0.2; + let targetWidth = (_b = this.options.logoSplashTargetWidth) !== null && _b !== void 0 ? _b : Math.floor(((_c = splash.width) !== null && _c !== void 0 ? _c : 0) * targetLogoWidthPercent); + if (targetWidth > splash.width || targetWidth > splash.height) { + targetWidth = Math.floor(((_d = splash.width) !== null && _d !== void 0 ? _d : 0) * targetLogoWidthPercent); + } + if (targetWidth > splash.width || targetWidth > splash.height) { + (0, log_1.warn)(`Logo dimensions exceed dimensions of splash ${splash.width}x${splash.height}, using default logo size`); + targetWidth = Math.floor(((_e = splash.width) !== null && _e !== void 0 ? _e : 0) * 0.2); + } + const canvas = (0, sharp_1.default)({ + create: { + width: (_f = splash.width) !== null && _f !== void 0 ? _f : 0, + height: (_g = splash.height) !== null && _g !== void 0 ? _g : 0, + channels: 4, + background: backgroundColor, + }, + }); + const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); + const outputInfo = await canvas + .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) + .png() + .toFile(dest); + const splashOutput = new output_asset_1.OutputAsset(splash, asset, project, { + [dest]: dest, + }, { + [dest]: outputInfo, + }); + return splashOutput; + } + async _generateSplashesFromLogo(project, asset, splash, pipe, backgroundColor) { + var _a, _b, _c, _d, _e, _f, _g; + // Generate light splash + const resPath = this.getResPath(project); + let drawableDir = `drawable`; + if (splash.density) { + drawableDir = `drawable-${splash.density}`; + } + const parentDir = (0, path_1.join)(resPath, drawableDir); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const dest = (0, path_1.join)(resPath, drawableDir, 'splash.png'); + const targetLogoWidthPercent = (_a = this.options.logoSplashScale) !== null && _a !== void 0 ? _a : 0.2; + let targetWidth = (_b = this.options.logoSplashTargetWidth) !== null && _b !== void 0 ? _b : Math.floor(((_c = splash.width) !== null && _c !== void 0 ? _c : 0) * targetLogoWidthPercent); + if (targetWidth > splash.width || targetWidth > splash.height) { + targetWidth = Math.floor(((_d = splash.width) !== null && _d !== void 0 ? _d : 0) * targetLogoWidthPercent); + } + if (targetWidth > splash.width || targetWidth > splash.height) { + (0, log_1.warn)(`Logo dimensions exceed dimensions of splash ${splash.width}x${splash.height}, using default logo size`); + targetWidth = Math.floor(((_e = splash.width) !== null && _e !== void 0 ? _e : 0) * 0.2); + } + const canvas = (0, sharp_1.default)({ + create: { + width: (_f = splash.width) !== null && _f !== void 0 ? _f : 0, + height: (_g = splash.height) !== null && _g !== void 0 ? _g : 0, + channels: 4, + background: backgroundColor, + }, + }); + const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); + const outputInfo = await canvas + .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) + .png() + .toFile(dest); + const splashOutput = new output_asset_1.OutputAsset(splash, asset, project, { + [dest]: dest, + }, { + [dest]: outputInfo, + }); + return splashOutput; + } + async generateLegacyIcon(asset, project) { + const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "icon" /* Icon */); + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const collected = await Promise.all(icons.map(async (icon) => { + const [dest, outputInfo] = await this.generateLegacyLauncherIcon(project, asset, icon, pipe); + return new output_asset_1.OutputAsset(icon, asset, project, { [`mipmap-${icon.density}/ic_launcher.png`]: dest }, { [`mipmap-${icon.density}/ic_launcher.png`]: outputInfo }); + })); + collected.push(...(await Promise.all(icons.map(async (icon) => { + const [dest, outputInfo] = await this.generateRoundLauncherIcon(project, asset, icon, pipe); + return new output_asset_1.OutputAsset(icon, asset, project, { [`mipmap-${icon.density}/ic_launcher_round.png`]: dest }, { [`mipmap-${icon.density}/ic_launcher_round.png`]: outputInfo }); + })))); + await this.updateManifest(project); + return collected; + } + async generateLegacyLauncherIcon(project, asset, template, pipe) { + const radius = 4; + const svg = ``; + const resPath = this.getResPath(project); + const parentDir = (0, path_1.join)(resPath, `mipmap-${template.density}`); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const destRound = (0, path_1.join)(resPath, `mipmap-${template.density}`, 'ic_launcher.png'); + // This pipeline is trick, but we need two separate pipelines + // per https://github.com/lovell/sharp/issues/2378#issuecomment-864132578 + const padding = 8; + const resized = await (0, sharp_1.default)(asset.path) + .resize(template.width, template.height) + // .composite([{ input: Buffer.from(svg), blend: 'dest-in' }]) + .toBuffer(); + const composited = await (0, sharp_1.default)(resized) + .resize(Math.max(0, template.width - padding * 2), Math.max(0, template.height - padding * 2)) + .extend({ + top: padding, + bottom: padding, + left: padding, + right: padding, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .toBuffer(); + const outputInfo = await (0, sharp_1.default)(composited).png().toFile(destRound); + return [destRound, outputInfo]; + } + async generateRoundLauncherIcon(project, asset, template, pipe) { + const svg = ``; + const resPath = this.getResPath(project); + const destRound = (0, path_1.join)(resPath, `mipmap-${template.density}`, 'ic_launcher_round.png'); + // This pipeline is tricky, but we need two separate pipelines + // per https://github.com/lovell/sharp/issues/2378#issuecomment-864132578 + const resized = await (0, sharp_1.default)(asset.path).resize(template.width, template.height).toBuffer(); + const composited = await (0, sharp_1.default)(resized) + .composite([{ input: Buffer.from(svg), blend: 'dest-in' }]) + .toBuffer(); + const outputInfo = await (0, sharp_1.default)(composited).png().toFile(destRound); + return [destRound, outputInfo]; + } + async generateAdaptiveIconForeground(asset, project) { + const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "icon" /* Icon */); + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + return Promise.all(icons.map(async (icon) => { + return await this._generateAdaptiveIconForeground(project, asset, icon, pipe); + })); + } + async _generateAdaptiveIconForeground(project, asset, icon, pipe) { + const resPath = this.getResPath(project); + // Create the foreground and background images + const destForeground = (0, path_1.join)(resPath, `mipmap-${icon.density}`, 'ic_launcher_foreground.png'); + const parentDir = (0, path_1.dirname)(destForeground); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const outputInfoForeground = await pipe.resize(icon.width, icon.height).png().toFile(destForeground); + // Create the adaptive icon XML + const icLauncherXml = ` + + + + + + + + + + `.trim(); + const mipmapAnyPath = (0, path_1.join)(resPath, `mipmap-anydpi-v26`); + if (!(await (0, utils_fs_1.pathExists)(mipmapAnyPath))) { + await (0, utils_fs_1.mkdirp)(mipmapAnyPath); + } + const destIcLauncher = (0, path_1.join)(mipmapAnyPath, `ic_launcher.xml`); + const destIcLauncherRound = (0, path_1.join)(mipmapAnyPath, `ic_launcher_round.xml`); + await (0, utils_fs_1.writeFile)(destIcLauncher, icLauncherXml); + await (0, utils_fs_1.writeFile)(destIcLauncherRound, icLauncherXml); + // Return the created files for this OutputAsset + return new output_asset_1.OutputAsset(icon, asset, project, { + [`mipmap-${icon.density}/ic_launcher_foreground.png`]: destForeground, + 'mipmap-anydpi-v26/ic_launcher.xml': destIcLauncher, + 'mipmap-anydpi-v26/ic_launcher_round.xml': destIcLauncherRound, + }, { + [`mipmap-${icon.density}/ic_launcher_foreground.png`]: outputInfoForeground, + }); + } + async generateAdaptiveIconBackground(asset, project) { + const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "icon" /* Icon */); + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + return Promise.all(icons.map(async (icon) => { + return await this._generateAdaptiveIconBackground(project, asset, icon, pipe); + })); + } + async _generateAdaptiveIconBackground(project, asset, icon, pipe) { + const resPath = this.getResPath(project); + const destBackground = (0, path_1.join)(resPath, `mipmap-${icon.density}`, 'ic_launcher_background.png'); + const parentDir = (0, path_1.dirname)(destBackground); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const outputInfoBackground = await pipe.resize(icon.width, icon.height).png().toFile(destBackground); + // Create the adaptive icon XML + const icLauncherXml = ` + + + + + + + + + + `.trim(); + const mipmapAnyPath = (0, path_1.join)(resPath, `mipmap-anydpi-v26`); + if (!(await (0, utils_fs_1.pathExists)(mipmapAnyPath))) { + await (0, utils_fs_1.mkdirp)(mipmapAnyPath); + } + const destIcLauncher = (0, path_1.join)(mipmapAnyPath, `ic_launcher.xml`); + const destIcLauncherRound = (0, path_1.join)(mipmapAnyPath, `ic_launcher_round.xml`); + await (0, utils_fs_1.writeFile)(destIcLauncher, icLauncherXml); + await (0, utils_fs_1.writeFile)(destIcLauncherRound, icLauncherXml); + // Return the created files for this OutputAsset + return new output_asset_1.OutputAsset(icon, asset, project, { + [`mipmap-${icon.density}/ic_launcher_background.png`]: destBackground, + 'mipmap-anydpi-v26/ic_launcher.xml': destIcLauncher, + 'mipmap-anydpi-v26/ic_launcher_round.xml': destIcLauncherRound, + }, { + [`mipmap-${icon.density}/ic_launcher_background.png`]: outputInfoBackground, + }); + } + async updateManifest(project) { + var _a, _b; + (_b = (_a = project.android) === null || _a === void 0 ? void 0 : _a.getAndroidManifest()) === null || _b === void 0 ? void 0 : _b.setAttrs('manifest/application', { + 'android:icon': '@mipmap/ic_launcher', + 'android:banner': '@drawable/banner', + 'android:roundIcon': '@mipmap/ic_launcher_round', + }); + await project.commit(); + } + async generateBanners(asset, project) { + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const banners = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "banner" /* Banner */); + const resPath = this.getResPath(project); + const collected = await Promise.all(banners.map(async (banner) => { + const [dest, outputInfo] = await this.generateBanner(project, asset, banner, pipe); + const relPath = (0, path_1.relative)(resPath, dest); + return new output_asset_1.OutputAsset(banner, asset, project, { [relPath]: dest }, { [relPath]: outputInfo }); + })); + return collected; + } + async generateBanner(project, asset, template, pipe) { + const drawableDir = template.density ? `drawable-${template.density}` : 'drawable'; + const resPath = this.getResPath(project); + const parentDir = (0, path_1.join)(resPath, drawableDir); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const dest = (0, path_1.join)(resPath, drawableDir, 'banner.png'); + const outputInfo = await pipe.resize(template.width, template.height).png().toFile(dest); + return [dest, outputInfo]; + } + async generateSplashes(asset, project) { + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const splashes = (asset.kind === "splash" /* Splash */ + ? Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash" /* Splash */) + : Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash-dark" /* SplashDark */)); + const resPath = this.getResPath(project); + const collected = await Promise.all(splashes.map(async (splash) => { + const [dest, outputInfo] = await this.generateSplash(project, asset, splash, pipe); + const relPath = (0, path_1.relative)(resPath, dest); + return new output_asset_1.OutputAsset(splash, asset, project, { [relPath]: dest }, { [relPath]: outputInfo }); + })); + return collected; + } + async generateSplash(project, asset, template, pipe) { + const drawableDir = template.density ? `drawable-${template.density}` : 'drawable'; + const resPath = this.getResPath(project); + const parentDir = (0, path_1.join)(resPath, drawableDir); + if (!(await (0, utils_fs_1.pathExists)(parentDir))) { + await (0, utils_fs_1.mkdirp)(parentDir); + } + const dest = (0, path_1.join)(resPath, drawableDir, 'splash.png'); + const outputInfo = await pipe.resize(template.width, template.height).png().toFile(dest); + return [dest, outputInfo]; + } + getResPath(project) { + var _a; + return (0, path_1.join)(project.config.android.path, 'app', 'src', (_a = this.options.androidFlavor) !== null && _a !== void 0 ? _a : 'main', 'res'); + } +} +exports.AndroidAssetGenerator = AndroidAssetGenerator; diff --git a/dist/platforms/ios/assets.d.ts b/dist/platforms/ios/assets.d.ts new file mode 100644 index 0000000..ecc654f --- /dev/null +++ b/dist/platforms/ios/assets.d.ts @@ -0,0 +1,13 @@ +import type { IosOutputAssetTemplate, IosOutputAssetTemplateSplash } from '../../definitions'; +/** + * 1024px Icon + * + * - iOS 1024 icon + */ +export declare const IOS_1024_ICON: IosOutputAssetTemplate; +export declare const IOS_1X_UNIVERSAL_ANYANY_SPLASH: IosOutputAssetTemplateSplash; +export declare const IOS_2X_UNIVERSAL_ANYANY_SPLASH: IosOutputAssetTemplateSplash; +export declare const IOS_3X_UNIVERSAL_ANYANY_SPLASH: IosOutputAssetTemplateSplash; +export declare const IOS_1X_UNIVERSAL_ANYANY_SPLASH_DARK: IosOutputAssetTemplateSplash; +export declare const IOS_2X_UNIVERSAL_ANYANY_SPLASH_DARK: IosOutputAssetTemplateSplash; +export declare const IOS_3X_UNIVERSAL_ANYANY_SPLASH_DARK: IosOutputAssetTemplateSplash; diff --git a/dist/platforms/ios/assets.js b/dist/platforms/ios/assets.js new file mode 100644 index 0000000..4a54b9e --- /dev/null +++ b/dist/platforms/ios/assets.js @@ -0,0 +1,89 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IOS_3X_UNIVERSAL_ANYANY_SPLASH_DARK = exports.IOS_2X_UNIVERSAL_ANYANY_SPLASH_DARK = exports.IOS_1X_UNIVERSAL_ANYANY_SPLASH_DARK = exports.IOS_3X_UNIVERSAL_ANYANY_SPLASH = exports.IOS_2X_UNIVERSAL_ANYANY_SPLASH = exports.IOS_1X_UNIVERSAL_ANYANY_SPLASH = exports.IOS_1024_ICON = void 0; +/** + * 1024px Icon + * + * - iOS 1024 icon + */ +exports.IOS_1024_ICON = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "icon" /* Icon */, + name: 'AppIcon-512@2x.png', + format: "png" /* Png */, + width: 1024, + height: 1024, +}; +exports.IOS_1X_UNIVERSAL_ANYANY_SPLASH = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "splash" /* Splash */, + name: 'Default@1x~universal~anyany.png', + format: "png" /* Png */, + width: 2732, + height: 2732, + orientation: "portrait" /* Portrait */, + scale: 1, + theme: "any" /* Any */, +}; +exports.IOS_2X_UNIVERSAL_ANYANY_SPLASH = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "splash" /* Splash */, + name: 'Default@2x~universal~anyany.png', + format: "png" /* Png */, + width: 2732, + height: 2732, + orientation: "portrait" /* Portrait */, + scale: 2, + theme: "any" /* Any */, +}; +exports.IOS_3X_UNIVERSAL_ANYANY_SPLASH = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "splash" /* Splash */, + name: 'Default@3x~universal~anyany.png', + format: "png" /* Png */, + width: 2732, + height: 2732, + orientation: "portrait" /* Portrait */, + scale: 3, + theme: "any" /* Any */, +}; +exports.IOS_1X_UNIVERSAL_ANYANY_SPLASH_DARK = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "splash-dark" /* SplashDark */, + name: 'Default@1x~universal~anyany-dark.png', + format: "png" /* Png */, + width: 2732, + height: 2732, + orientation: "portrait" /* Portrait */, + scale: 1, + theme: "dark" /* Dark */, +}; +exports.IOS_2X_UNIVERSAL_ANYANY_SPLASH_DARK = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "splash-dark" /* SplashDark */, + name: 'Default@2x~universal~anyany-dark.png', + format: "png" /* Png */, + width: 2732, + height: 2732, + orientation: "portrait" /* Portrait */, + scale: 2, + theme: "dark" /* Dark */, +}; +exports.IOS_3X_UNIVERSAL_ANYANY_SPLASH_DARK = { + platform: "ios" /* Ios */, + idiom: "universal" /* Universal */, + kind: "splash-dark" /* SplashDark */, + name: 'Default@3x~universal~anyany-dark.png', + format: "png" /* Png */, + width: 2732, + height: 2732, + orientation: "portrait" /* Portrait */, + scale: 3, + theme: "dark" /* Dark */, +}; diff --git a/dist/platforms/ios/index.d.ts b/dist/platforms/ios/index.d.ts new file mode 100644 index 0000000..1b061b5 --- /dev/null +++ b/dist/platforms/ios/index.d.ts @@ -0,0 +1,21 @@ +import type { AssetGeneratorOptions } from '../../asset-generator'; +import { AssetGenerator } from '../../asset-generator'; +import type { InputAsset } from '../../input-asset'; +import { OutputAsset } from '../../output-asset'; +import type { Project } from '../../project'; +export declare const IOS_APP_ICON_SET_NAME = "AppIcon"; +export declare const IOS_APP_ICON_SET_PATH: string; +export declare const IOS_SPLASH_IMAGE_SET_NAME = "Splash"; +export declare const IOS_SPLASH_IMAGE_SET_PATH: string; +export declare class IosAssetGenerator extends AssetGenerator { + constructor(options?: AssetGeneratorOptions); + generate(asset: InputAsset, project: Project): Promise; + private generateFromLogo; + private _generateIcons; + private generateIconsForLogo; + private generateIcons; + private generateSplashes; + private updateIconsContentsJson; + private updateSplashContentsJson; + private updateSplashContentsJsonDark; +} diff --git a/dist/platforms/ios/index.js b/dist/platforms/ios/index.js new file mode 100644 index 0000000..b7ba543 --- /dev/null +++ b/dist/platforms/ios/index.js @@ -0,0 +1,266 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IosAssetGenerator = exports.IOS_SPLASH_IMAGE_SET_PATH = exports.IOS_SPLASH_IMAGE_SET_NAME = exports.IOS_APP_ICON_SET_PATH = exports.IOS_APP_ICON_SET_NAME = void 0; +const tslib_1 = require("tslib"); +const utils_fs_1 = require("@ionic/utils-fs"); +const path_1 = require("path"); +const sharp_1 = (0, tslib_1.__importDefault)(require("sharp")); +const asset_generator_1 = require("../../asset-generator"); +const error_1 = require("../../error"); +const output_asset_1 = require("../../output-asset"); +const assets_1 = require("./assets"); +const IosAssetTemplates = (0, tslib_1.__importStar)(require("./assets")); +exports.IOS_APP_ICON_SET_NAME = 'AppIcon'; +exports.IOS_APP_ICON_SET_PATH = `App/Assets.xcassets/${exports.IOS_APP_ICON_SET_NAME}.appiconset`; +exports.IOS_SPLASH_IMAGE_SET_NAME = 'Splash'; +exports.IOS_SPLASH_IMAGE_SET_PATH = `App/Assets.xcassets/${exports.IOS_SPLASH_IMAGE_SET_NAME}.imageset`; +class IosAssetGenerator extends asset_generator_1.AssetGenerator { + constructor(options = {}) { + super(options); + } + async generate(asset, project) { + var _a; + const iosDir = (_a = project.config.ios) === null || _a === void 0 ? void 0 : _a.path; + if (!iosDir) { + throw new error_1.BadProjectError('No ios project found'); + } + if (asset.platform !== "any" /* Any */ && asset.platform !== "ios" /* Ios */) { + return []; + } + switch (asset.kind) { + case "logo" /* Logo */: + case "logo-dark" /* LogoDark */: + return this.generateFromLogo(asset, project); + case "icon" /* Icon */: + return this.generateIcons(asset, project); + case "splash" /* Splash */: + case "splash-dark" /* SplashDark */: + return this.generateSplashes(asset, project); + } + return []; + } + async generateFromLogo(asset, project) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const iosDir = project.config.ios.path; + // Generate logos + let logos = []; + if (asset.kind === "logo" /* Logo */) { + logos = await this.generateIconsForLogo(asset, project); + } + const generated = []; + const targetLogoWidthPercent = (_a = this.options.logoSplashScale) !== null && _a !== void 0 ? _a : 0.2; + const targetWidth = (_b = this.options.logoSplashTargetWidth) !== null && _b !== void 0 ? _b : Math.floor(((_c = asset.width) !== null && _c !== void 0 ? _c : 0) * targetLogoWidthPercent); + if (asset.kind === "logo" /* Logo */) { + // Generate light splash + const lightDefaultBackground = '#ffffff'; + const lightSplashes = [ + assets_1.IOS_1X_UNIVERSAL_ANYANY_SPLASH, + assets_1.IOS_2X_UNIVERSAL_ANYANY_SPLASH, + assets_1.IOS_3X_UNIVERSAL_ANYANY_SPLASH, + ]; + const lightSplashesGenerated = []; + for (const lightSplash of lightSplashes) { + const lightDest = (0, path_1.join)(iosDir, exports.IOS_SPLASH_IMAGE_SET_PATH, lightSplash.name); + const canvas = (0, sharp_1.default)({ + create: { + width: (_d = lightSplash.width) !== null && _d !== void 0 ? _d : 0, + height: (_e = lightSplash.height) !== null && _e !== void 0 ? _e : 0, + channels: 4, + background: (_f = this.options.splashBackgroundColor) !== null && _f !== void 0 ? _f : lightDefaultBackground, + }, + }); + const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); + const lightOutputInfo = await canvas + .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) + .png() + .toFile(lightDest); + const lightSplashOutput = new output_asset_1.OutputAsset(lightSplash, asset, project, { + [lightDest]: lightDest, + }, { + [lightDest]: lightOutputInfo, + }); + generated.push(lightSplashOutput); + lightSplashesGenerated.push(lightSplashOutput); + } + await this.updateSplashContentsJson(lightSplashesGenerated, project); + } + // Generate dark splash + const darkDefaultBackground = '#111111'; + const darkSplashes = [ + assets_1.IOS_1X_UNIVERSAL_ANYANY_SPLASH_DARK, + assets_1.IOS_2X_UNIVERSAL_ANYANY_SPLASH_DARK, + assets_1.IOS_3X_UNIVERSAL_ANYANY_SPLASH_DARK, + ]; + const darkSplashesGenerated = []; + for (const darkSplash of darkSplashes) { + const darkDest = (0, path_1.join)(iosDir, exports.IOS_SPLASH_IMAGE_SET_PATH, darkSplash.name); + const canvas = (0, sharp_1.default)({ + create: { + width: (_g = darkSplash.width) !== null && _g !== void 0 ? _g : 0, + height: (_h = darkSplash.height) !== null && _h !== void 0 ? _h : 0, + channels: 4, + background: (_j = this.options.splashBackgroundColorDark) !== null && _j !== void 0 ? _j : darkDefaultBackground, + }, + }); + const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); + const darkOutputInfo = await canvas + .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) + .png() + .toFile(darkDest); + const darkSplashOutput = new output_asset_1.OutputAsset(darkSplash, asset, project, { + [darkDest]: darkDest, + }, { + [darkDest]: darkOutputInfo, + }); + generated.push(darkSplashOutput); + darkSplashesGenerated.push(darkSplashOutput); + } + await this.updateSplashContentsJsonDark(darkSplashesGenerated, project); + return [...logos, ...generated]; + } + async _generateIcons(asset, project, icons) { + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const iosDir = project.config.ios.path; + const lightDefaultBackground = '#ffffff'; + const generated = await Promise.all(icons.map(async (icon) => { + var _a; + const dest = (0, path_1.join)(iosDir, exports.IOS_APP_ICON_SET_PATH, icon.name); + const outputInfo = await pipe + .resize(icon.width, icon.height) + .png() + .flatten({ background: (_a = this.options.iconBackgroundColor) !== null && _a !== void 0 ? _a : lightDefaultBackground }) + .toFile(dest); + return new output_asset_1.OutputAsset(icon, asset, project, { + [icon.name]: dest, + }, { + [icon.name]: outputInfo, + }); + })); + await this.updateIconsContentsJson(generated, project); + return generated; + } + // Generate ALL the icons when only given a logo + async generateIconsForLogo(asset, project) { + const icons = Object.values(IosAssetTemplates).filter((a) => ["icon" /* Icon */].find((i) => i === a.kind)); + return this._generateIcons(asset, project, icons); + } + async generateIcons(asset, project) { + const icons = Object.values(IosAssetTemplates).filter((a) => ["icon" /* Icon */].find((i) => i === a.kind)); + return this._generateIcons(asset, project, icons); + } + async generateSplashes(asset, project) { + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const assetMetas = asset.kind === "splash" /* Splash */ + ? [assets_1.IOS_1X_UNIVERSAL_ANYANY_SPLASH, assets_1.IOS_2X_UNIVERSAL_ANYANY_SPLASH, assets_1.IOS_3X_UNIVERSAL_ANYANY_SPLASH] + : [ + assets_1.IOS_1X_UNIVERSAL_ANYANY_SPLASH_DARK, + assets_1.IOS_2X_UNIVERSAL_ANYANY_SPLASH_DARK, + assets_1.IOS_3X_UNIVERSAL_ANYANY_SPLASH_DARK, + ]; + const generated = []; + for (const assetMeta of assetMetas) { + const iosDir = project.config.ios.path; + const dest = (0, path_1.join)(iosDir, exports.IOS_SPLASH_IMAGE_SET_PATH, assetMeta.name); + const outputInfo = await pipe.resize(assetMeta.width, assetMeta.height).png().toFile(dest); + const g = new output_asset_1.OutputAsset(assetMeta, asset, project, { + [assetMeta.name]: dest, + }, { + [assetMeta.name]: outputInfo, + }); + generated.push(g); + } + if (asset.kind === "splash" /* Splash */) { + await this.updateSplashContentsJson(generated, project); + } + else if (asset.kind === "splash-dark" /* SplashDark */) { + // Need to register this as a dark-mode splash + await this.updateSplashContentsJsonDark(generated, project); + } + return generated; + } + async updateIconsContentsJson(generated, project) { + const assetsPath = (0, path_1.join)(project.config.ios.path, exports.IOS_APP_ICON_SET_PATH); + const contentsJsonPath = (0, path_1.join)(assetsPath, 'Contents.json'); + const json = await (0, utils_fs_1.readFile)(contentsJsonPath, { encoding: 'utf-8' }); + const parsed = JSON.parse(json); + const withoutMissing = []; + for (const g of generated) { + const width = g.template.width; + const height = g.template.height; + parsed.images.map((i) => { + if (i.filename !== g.template.name) { + (0, utils_fs_1.rmSync)((0, path_1.join)(assetsPath, i.filename)); + } + }); + withoutMissing.push({ + idiom: g.template.idiom, + size: `${width}x${height}`, + filename: g.template.name, + platform: "ios" /* Ios */, + }); + } + parsed.images = withoutMissing; + await (0, utils_fs_1.writeFile)(contentsJsonPath, JSON.stringify(parsed, null, 2)); + } + async updateSplashContentsJson(generated, project) { + var _a; + const contentsJsonPath = (0, path_1.join)(project.config.ios.path, exports.IOS_SPLASH_IMAGE_SET_PATH, 'Contents.json'); + const json = await (0, utils_fs_1.readFile)(contentsJsonPath, { encoding: 'utf-8' }); + const parsed = JSON.parse(json); + const withoutMissing = parsed.images.filter((i) => !!i.filename); + for (const g of generated) { + const existing = withoutMissing.find((f) => f.scale === `${g.template.scale}x` && f.idiom === 'universal' && typeof f.appearances === 'undefined'); + if (existing) { + existing.filename = g.template.name; + } + else { + withoutMissing.push({ + idiom: 'universal', + scale: `${(_a = g.template.scale) !== null && _a !== void 0 ? _a : 1}x`, + filename: g.template.name, + }); + } + } + parsed.images = withoutMissing; + await (0, utils_fs_1.writeFile)(contentsJsonPath, JSON.stringify(parsed, null, 2)); + } + async updateSplashContentsJsonDark(generated, project) { + var _a; + const contentsJsonPath = (0, path_1.join)(project.config.ios.path, exports.IOS_SPLASH_IMAGE_SET_PATH, 'Contents.json'); + const json = await (0, utils_fs_1.readFile)(contentsJsonPath, { encoding: 'utf-8' }); + const parsed = JSON.parse(json); + const withoutMissing = parsed.images.filter((i) => !!i.filename); + for (const g of generated) { + const existing = withoutMissing.find((f) => f.scale === `${g.template.scale}x` && f.idiom === 'universal' && typeof f.appearances !== 'undefined'); + if (existing) { + existing.filename = g.template.name; + } + else { + withoutMissing.push({ + appearances: [ + { + appearance: 'luminosity', + value: 'dark', + }, + ], + idiom: 'universal', + scale: `${(_a = g.template.scale) !== null && _a !== void 0 ? _a : 1}x`, + filename: g.template.name, + }); + } + } + parsed.images = withoutMissing; + await (0, utils_fs_1.writeFile)(contentsJsonPath, JSON.stringify(parsed, null, 2)); + } +} +exports.IosAssetGenerator = IosAssetGenerator; diff --git a/dist/platforms/pwa/assets.d.ts b/dist/platforms/pwa/assets.d.ts new file mode 100644 index 0000000..8019d44 --- /dev/null +++ b/dist/platforms/pwa/assets.d.ts @@ -0,0 +1,20 @@ +import type { PwaOutputAssetTemplate } from '../../definitions'; +export declare const PWA_48_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_72_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_96_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_128_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_192_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_256_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_512_PX_ICON: PwaOutputAssetTemplate; +export declare const PWA_SPLASH: PwaOutputAssetTemplate; +export declare const ASSETS: { + PWA_48_PX_ICON: PwaOutputAssetTemplate; + PWA_72_PX_ICON: PwaOutputAssetTemplate; + PWA_96_PX_ICON: PwaOutputAssetTemplate; + PWA_128_PX_ICON: PwaOutputAssetTemplate; + PWA_192_PX_ICON: PwaOutputAssetTemplate; + PWA_256_PX_ICON: PwaOutputAssetTemplate; + PWA_512_PX_ICON: PwaOutputAssetTemplate; + PWA_SPLASH: PwaOutputAssetTemplate; +}; +export declare const PWA_IOS_DEVICE_SIZES: string[]; diff --git a/dist/platforms/pwa/assets.js b/dist/platforms/pwa/assets.js new file mode 100644 index 0000000..697292e --- /dev/null +++ b/dist/platforms/pwa/assets.js @@ -0,0 +1,93 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PWA_IOS_DEVICE_SIZES = exports.ASSETS = exports.PWA_SPLASH = exports.PWA_512_PX_ICON = exports.PWA_256_PX_ICON = exports.PWA_192_PX_ICON = exports.PWA_128_PX_ICON = exports.PWA_96_PX_ICON = exports.PWA_72_PX_ICON = exports.PWA_48_PX_ICON = void 0; +exports.PWA_48_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-48.webp', + format: "webp" /* WebP */, + width: 48, + height: 48, +}; +exports.PWA_72_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-72.webp', + format: "webp" /* WebP */, + width: 72, + height: 72, +}; +exports.PWA_96_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-96.webp', + format: "webp" /* WebP */, + width: 96, + height: 96, +}; +exports.PWA_128_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-128.webp', + format: "webp" /* WebP */, + width: 128, + height: 128, +}; +exports.PWA_192_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-192.webp', + format: "webp" /* WebP */, + width: 192, + height: 192, +}; +exports.PWA_256_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-256.webp', + format: "webp" /* WebP */, + width: 256, + height: 256, +}; +exports.PWA_512_PX_ICON = { + platform: "pwa" /* Pwa */, + kind: "icon" /* Icon */, + name: 'icon-512.webp', + format: "webp" /* WebP */, + width: 512, + height: 512, +}; +exports.PWA_SPLASH = { + platform: "pwa" /* Pwa */, + kind: "splash" /* Splash */, + name: 'apple-splash.webp', + format: "webp" /* WebP */, + width: 2048, + height: 2048, +}; +exports.ASSETS = { + PWA_48_PX_ICON: exports.PWA_48_PX_ICON, + PWA_72_PX_ICON: exports.PWA_72_PX_ICON, + PWA_96_PX_ICON: exports.PWA_96_PX_ICON, + PWA_128_PX_ICON: exports.PWA_128_PX_ICON, + PWA_192_PX_ICON: exports.PWA_192_PX_ICON, + PWA_256_PX_ICON: exports.PWA_256_PX_ICON, + PWA_512_PX_ICON: exports.PWA_512_PX_ICON, + PWA_SPLASH: exports.PWA_SPLASH, +}; +// From https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/ +exports.PWA_IOS_DEVICE_SIZES = [ + '2048x2732@2x', + '1668x2388@2x', + '1668x2224@2x', + '1620x2160@2x', + '1536x2048@2x', + '1284x2778@3x', + '1242x2688@3x', + '1170x2532@3x', + '1125x2436@3x', + '1080x1920@3x', + '828x1792@2x', + '750x1334@2x', + '640x1136@2x', +]; diff --git a/dist/platforms/pwa/index.d.ts b/dist/platforms/pwa/index.d.ts new file mode 100644 index 0000000..d99a05d --- /dev/null +++ b/dist/platforms/pwa/index.d.ts @@ -0,0 +1,31 @@ +import type { AssetGeneratorOptions } from '../../asset-generator'; +import { AssetGenerator } from '../../asset-generator'; +import type { InputAsset } from '../../input-asset'; +import { OutputAsset } from '../../output-asset'; +import type { Project } from '../../project'; +export declare const PWA_ASSET_PATH = "icons"; +export interface ManifestIcon { + src: string; + size?: string | number; + sizes?: string; + destination?: string; + purpose?: string; + type?: string; +} +export declare class PwaAssetGenerator extends AssetGenerator { + constructor(options?: AssetGeneratorOptions); + getManifestJson(project: Project): Promise; + getSplashSizes(): Promise; + generate(asset: InputAsset, project: Project): Promise; + private generateFromLogo; + private _generateSplashFromLogo; + private generateIcons; + private getPWADirectory; + private getPWAAssetsDirectory; + private getManifestJsonPath; + private updateManifest; + private makeIconManifestEntry; + private generateSplashes; + private _generateSplash; + static logInstructions(generated: OutputAsset[]): void; +} diff --git a/dist/platforms/pwa/index.js b/dist/platforms/pwa/index.js new file mode 100644 index 0000000..105a0e5 --- /dev/null +++ b/dist/platforms/pwa/index.js @@ -0,0 +1,425 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PwaAssetGenerator = exports.PWA_ASSET_PATH = void 0; +const tslib_1 = require("tslib"); +const utils_fs_1 = require("@ionic/utils-fs"); +const node_fetch_1 = (0, tslib_1.__importDefault)(require("node-fetch")); +const node_html_parser_1 = (0, tslib_1.__importDefault)(require("node-html-parser")); +const path_1 = require("path"); +const sharp_1 = (0, tslib_1.__importDefault)(require("sharp")); +const asset_generator_1 = require("../../asset-generator"); +const error_1 = require("../../error"); +const output_asset_1 = require("../../output-asset"); +const log_1 = require("../../util/log"); +const assets_1 = require("./assets"); +exports.PWA_ASSET_PATH = 'icons'; +class PwaAssetGenerator extends asset_generator_1.AssetGenerator { + constructor(options = {}) { + super(options); + } + async getManifestJson(project) { + var _a; + const path = await this.getManifestJsonPath((_a = project.directory) !== null && _a !== void 0 ? _a : ''); + const contents = await (0, utils_fs_1.readFile)(path, { encoding: 'utf-8' }); + return JSON.parse(contents); + } + async getSplashSizes() { + var _a; + const appleInterfacePage = `https://developer.apple.com/design/human-interface-guidelines/foundations/layout/`; + let assetSizes = assets_1.PWA_IOS_DEVICE_SIZES; + if (!this.options.pwaNoAppleFetch) { + try { + const res = await (0, node_fetch_1.default)(appleInterfacePage); + const html = await res.text(); + const doc = (0, node_html_parser_1.default)(html); + const target = doc.querySelector('main > section .row > .column table'); + const sizes = (_a = target === null || target === void 0 ? void 0 : target.querySelectorAll('tr > td:nth-child(2)')) !== null && _a !== void 0 ? _a : []; + const sizeStrings = sizes.map((td) => { + const t = td.innerText; + return t + .slice(t.indexOf('pt (') + 4) + .slice(0, -1) + .replace(' px ', ''); + }); + const deduped = new Set(sizeStrings); + assetSizes = Array.from(deduped); + } + catch (e) { + (0, log_1.warn)(`Unable to load iOS HIG screen sizes to generate iOS PWA splash screens. Using local snapshot of device sizes. Use --pwaNoAppleFetch true to always use local sizes`); + } + } + return assetSizes; + } + async generate(asset, project) { + const pwaDir = project.directory; + if (!pwaDir) { + throw new error_1.BadProjectError('No web app (PWA) found'); + } + if (asset.platform !== "any" /* Any */) { + return []; + } + switch (asset.kind) { + case "logo" /* Logo */: + case "logo-dark" /* LogoDark */: + return this.generateFromLogo(asset, project); + case "icon" /* Icon */: + return this.generateIcons(asset, project); + // eslint-disable-next-line no-duplicate-case + case "icon" /* Icon */: + return []; + case "splash" /* Splash */: + case "splash-dark" /* SplashDark */: + // PWA has no splashes + return this.generateSplashes(asset, project); + } + return []; + } + async generateFromLogo(asset, project) { + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + // Generate logos + const logos = await this.generateIcons(asset, project); + const assetSizes = await this.getSplashSizes(); + const generated = []; + const splashes = await Promise.all(assetSizes.map((a) => this._generateSplashFromLogo(project, asset, a, pipe))); + generated.push(...splashes.flat()); + return [...logos, ...generated]; + } + async _generateSplashFromLogo(project, asset, sizeString, pipe) { + var _a, _b, _c; + const parts = sizeString.split('@'); + const sizeParts = parts[0].split('x'); + const width = parseFloat(sizeParts[0]); + const height = parseFloat(sizeParts[1]); + const density = parts[1]; + const generated = []; + const pwaDir = await this.getPWADirectory((_a = project.directory) !== null && _a !== void 0 ? _a : undefined); + const pwaAssetDir = await this.getPWAAssetsDirectory(pwaDir); + const destDir = (0, path_1.join)(pwaAssetDir, exports.PWA_ASSET_PATH); + try { + await (0, utils_fs_1.mkdirp)(destDir); + } + catch { + // ignore error + } + // TODO: In the future, add size checks to ensure canvas image + // is not exceeded (see Android splash generation) + const targetLogoWidthPercent = (_b = this.options.logoSplashScale) !== null && _b !== void 0 ? _b : 0.2; + const targetWidth = (_c = this.options.logoSplashTargetWidth) !== null && _c !== void 0 ? _c : Math.floor(width * targetLogoWidthPercent); + if (asset.kind === "logo" /* Logo */) { + // Generate light splash + const lightDefaultBackground = '#ffffff'; + const lightDest = (0, path_1.join)(destDir, `apple-splash-${width}-${height}@${density}.png`); + const canvas = (0, sharp_1.default)({ + create: { + width, + height, + channels: 4, + background: lightDefaultBackground, + }, + }); + const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); + const lightOutputInfo = await canvas + .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) + .png() + .toFile(lightDest); + const template = { + name: `apple-splash-${width}-${height}@${density}.png`, + platform: "pwa" /* Pwa */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + orientation: "portrait" /* Portrait */, + density: density[0], + width, + height, + }; + const lightSplashOutput = new output_asset_1.OutputAsset(template, asset, project, { + [lightDest]: lightDest, + }, { + [lightDest]: lightOutputInfo, + }); + generated.push(lightSplashOutput); + } + // Generate dark splash + const darkDefaultBackground = '#111111'; + const darkDest = (0, path_1.join)(destDir, `apple-splash-${width}-${height}@${density}-dark.png`); + const canvas = (0, sharp_1.default)({ + create: { + width, + height, + channels: 4, + background: darkDefaultBackground, + }, + }); + const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); + const darkOutputInfo = await canvas + .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) + .png() + .toFile(darkDest); + const template = { + name: `apple-splash-${width}-${height}@${density}-dark.png`, + platform: "pwa" /* Pwa */, + kind: "splash-dark" /* SplashDark */, + format: "png" /* Png */, + orientation: "portrait" /* Portrait */, + density: density[0], + width, + height, + }; + const darkSplashOutput = new output_asset_1.OutputAsset(template, asset, project, { + [darkDest]: darkDest, + }, { + [darkDest]: darkOutputInfo, + }); + generated.push(darkSplashOutput); + return generated; + } + async generateIcons(asset, project) { + var _a; + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const pwaDir = await this.getPWADirectory((_a = project.directory) !== null && _a !== void 0 ? _a : undefined); + const icons = Object.values(assets_1.ASSETS).filter((a) => a.kind === "icon" /* Icon */); + const generatedAssets = await Promise.all(icons.map(async (icon) => { + const destDir = (0, path_1.join)(await this.getPWAAssetsDirectory(pwaDir), exports.PWA_ASSET_PATH); + try { + await (0, utils_fs_1.mkdirp)(destDir); + } + catch { + // ignore error + } + const dest = (0, path_1.join)(destDir, icon.name); + const outputInfo = await pipe.resize(icon.width, icon.height).png().toFile(dest); + return new output_asset_1.OutputAsset(icon, asset, project, { + [icon.name]: dest, + }, { + [icon.name]: outputInfo, + }); + })); + await this.updateManifest(project, generatedAssets); + return generatedAssets; + } + async getPWADirectory(projectRoot) { + if (await (0, utils_fs_1.pathExists)((0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'public')) /* React */) { + return (0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'public'); + } + else if (await (0, utils_fs_1.pathExists)((0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'src/assets')) /* Angular and Vue */) { + return (0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'src/assets'); + } + else if (await (0, utils_fs_1.pathExists)((0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'www'))) { + return (0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'www'); + } + else { + return (0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', 'www'); + } + } + async getPWAAssetsDirectory(pwaDir) { + if (await (0, utils_fs_1.pathExists)((0, path_1.join)(pwaDir !== null && pwaDir !== void 0 ? pwaDir : '', 'assets'))) { + return (0, path_1.join)(pwaDir !== null && pwaDir !== void 0 ? pwaDir : '', 'assets'); + } + return ''; + } + async getManifestJsonPath(projectRoot) { + const r = (p) => (0, path_1.join)(projectRoot !== null && projectRoot !== void 0 ? projectRoot : '', p); + if (this.options.pwaManifestPath) { + return r(this.options.pwaManifestPath); + } + if (await (0, utils_fs_1.pathExists)(r('public'))) { + if (await (0, utils_fs_1.pathExists)(r('public/manifest.json'))) { + return r('public/manifest.json'); + } + // Default to the spec-preferred naming + return r('public/manifest.webmanifest'); + } + else if (await (0, utils_fs_1.pathExists)(r('src/assets'))) { + if (await (0, utils_fs_1.pathExists)(r('src/manifest.json'))) { + return r('src/manifest.json'); + } + // Default to the spec-preferred naming + return r('src/manifest.webmanifest'); + } + else if (await (0, utils_fs_1.pathExists)(r('www'))) { + if (await (0, utils_fs_1.pathExists)(r('www'))) { + return r('www/manifest.json'); + } + // Default to the spec-preferred naming + return r('www/manifest.webmanifest'); + } + else { + // Safe fallback to older styles + return r('www/manifest.json'); + } + } + async updateManifest(project, assets) { + var _a, _b; + const pwaDir = await this.getPWADirectory((_a = project.directory) !== null && _a !== void 0 ? _a : undefined); + const pwaAssetDir = await this.getPWAAssetsDirectory(pwaDir); + const manifestPath = await this.getManifestJsonPath((_b = project.directory) !== null && _b !== void 0 ? _b : undefined); + const pwaAssets = assets.filter((a) => a.template.platform === "pwa" /* Pwa */); + let manifestJson = {}; + if (await (0, utils_fs_1.pathExists)(manifestPath)) { + manifestJson = await (0, utils_fs_1.readJSON)(manifestPath); + } + const icons = manifestJson['icons'] || []; + for (const asset of pwaAssets) { + const src = asset.template.name; + const fname = (0, path_1.basename)(src); + const relativePath = (0, path_1.relative)(pwaDir, (0, path_1.join)(pwaAssetDir, exports.PWA_ASSET_PATH, fname)); + const existing = !!icons.find((i) => i.src === relativePath); + if (!existing) { + icons.push(this.makeIconManifestEntry(asset.template, relativePath)); + } + } + // Update the manifest background color to the splash one if provided to ensure + // platform automatic splash generation works + if (this.options.splashBackgroundColor) { + manifestJson['background_color'] = this.options.splashBackgroundColor; + } + const jsonOutput = { + ...manifestJson, + icons, + }; + await (0, utils_fs_1.writeJSON)(manifestPath, jsonOutput, { + spaces: 2, + }); + } + makeIconManifestEntry(asset, relativePath) { + const ext = (0, path_1.extname)(relativePath); + const posixPath = relativePath.split(path_1.sep).join(path_1.posix.sep); + const type = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + svg: 'image/svg+xml', + }[ext] || 'image/png'; + const entry = { + src: posixPath, + type, + sizes: `${asset.width}x${asset.height}`, + }; + if (asset.kind === "icon" /* Icon */) { + entry.purpose = 'any maskable'; + } + return entry; + } + async generateSplashes(asset, project) { + const pipe = asset.pipeline(); + if (!pipe) { + throw new error_1.BadPipelineError('Sharp instance not created'); + } + const assetSizes = await this.getSplashSizes(); + return Promise.all(assetSizes.map((a) => this._generateSplash(project, asset, a, pipe))); + } + async _generateSplash(project, asset, sizeString, pipe) { + var _a, _b; + const parts = sizeString.split('@'); + const sizeParts = parts[0].split('x'); + const width = parseFloat(sizeParts[0]); + const height = parseFloat(sizeParts[1]); + const density = parts[1]; + const name = `apple-splash-${width}-${height}@${density}${asset.kind === "splash-dark" /* SplashDark */ ? '-dark' : ''}.png`; + const pwaDir = await this.getPWADirectory((_a = project.directory) !== null && _a !== void 0 ? _a : undefined); + const pwaAssetDir = await this.getPWAAssetsDirectory(pwaDir); + const destDir = (0, path_1.join)(pwaAssetDir, exports.PWA_ASSET_PATH); + try { + await (0, utils_fs_1.mkdirp)(destDir); + } + catch { + // ignore error + } + const dest = (0, path_1.join)(destDir, name); + // console.log(width, height); + const targetLogoWidthPercent = (_b = this.options.logoSplashScale) !== null && _b !== void 0 ? _b : 0.2; + const targetWidth = Math.floor(width * targetLogoWidthPercent); + const outputInfo = await pipe.resize(width, height).png().toFile(dest); + const template = { + name, + platform: "pwa" /* Pwa */, + kind: "splash" /* Splash */, + format: "png" /* Png */, + orientation: "portrait" /* Portrait */, + density: density[0], + width, + height, + }; + const splashOutput = new output_asset_1.OutputAsset(template, asset, project, { + [dest]: dest, + }, { + [dest]: outputInfo, + }); + return splashOutput; + } + static logInstructions(generated) { + var _a, _b, _c, _d, _e, _f; + (0, log_1.log)(`PWA instructions: + +Add the following tags to your index.html to support PWA icons: +`); + const pwaAssets = generated.filter((g) => g.template.platform === "pwa" /* Pwa */); + const mainIcon = pwaAssets.find((g) => g.template.width == 512 && g.template.kind === "icon" /* Icon */); + (0, log_1.log)(``); + for (const g of pwaAssets.filter((a) => a.template.kind === "icon" /* Icon */)) { + const w = g.template.width; + const h = g.template.height; + const path = (_b = Object.values(g.destFilenames)[0]) !== null && _b !== void 0 ? _b : ''; + (0, log_1.log)(``); + } + for (const g of pwaAssets.filter((a) => a.template.kind === "splash" /* Splash */)) { + const template = g.template; + const w = g.template.width; + const h = g.template.height; + const path = (_c = Object.values(g.destFilenames)[0]) !== null && _c !== void 0 ? _c : ''; + (0, log_1.log)(``); + } + for (const g of pwaAssets.filter((a) => a.template.kind === "splash" /* Splash */)) { + const template = g.template; + const w = g.template.width; + const h = g.template.height; + const path = (_d = Object.values(g.destFilenames)[0]) !== null && _d !== void 0 ? _d : ''; + (0, log_1.log)(``); + } + for (const g of pwaAssets.filter((a) => a.template.kind === "splash-dark" /* SplashDark */)) { + const template = g.template; + const w = g.template.width; + const h = g.template.height; + const path = (_e = Object.values(g.destFilenames)[0]) !== null && _e !== void 0 ? _e : ''; + (0, log_1.log)(``); + } + for (const g of pwaAssets.filter((a) => a.template.kind === "splash-dark" /* SplashDark */)) { + const template = g.template; + const w = g.template.width; + const h = g.template.height; + const path = (_f = Object.values(g.destFilenames)[0]) !== null && _f !== void 0 ? _f : ''; + (0, log_1.log)(``); + } + console.log('Generated', pwaAssets.filter((a) => a.template.kind === "splash" /* Splash */).length, pwaAssets.filter((a) => a.template.kind === "splash-dark" /* SplashDark */).length); + /* + for (const g of pwaAssets.filter(a => a.template.kind === AssetKind.Splash)) { + const w = g.template.width; + const h = g.template.height; + const path = Object.values(g.destFilenames)[0] ?? ''; + log(`; diff --git a/dist/tasks/generate.js b/dist/tasks/generate.js new file mode 100644 index 0000000..735dde8 --- /dev/null +++ b/dist/tasks/generate.js @@ -0,0 +1,139 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = void 0; +const tslib_1 = require("tslib"); +const c = (0, tslib_1.__importStar)(require("../colors")); +const android_1 = require("../platforms/android"); +const ios_1 = require("../platforms/ios"); +const pwa_1 = require("../platforms/pwa"); +const log_1 = require("../util/log"); +async function run(ctx) { + try { + if (!(await ctx.project.assetDirExists())) { + (0, log_1.error)(`Asset directory not found at ${ctx.project.projectRoot}. Use --asset-path to specify a specific directory containing assets`); + return []; + } + const assets = await ctx.project.loadInputAssets(); + if ([assets.logo, assets.icon, assets.splash, assets.splashDark].every((a) => !a)) { + (0, log_1.error)(`No assets found in the asset path ${c.ancillary(ctx.project.assetDir)}. See https://github.com/ionic-team/capacitor-assets to learn how to use this tool.`); + return []; + } + let platforms = ['ios', 'android', 'pwa']; + if (ctx.args.ios || ctx.args.android || ctx.args.pwa) { + platforms = []; + } + if (ctx.args.ios) { + platforms.push('ios'); + } + if (ctx.args.android) { + platforms.push('android'); + } + if (ctx.args.pwa) { + platforms.push('pwa'); + } + await verifyPlatformFolders(/* mut */ platforms, ctx.project); + if (platforms.length > 0) { + if (!ctx.args.silent) { + (0, log_1.log)(`Generating assets for ${platforms.map((p) => c.strong(c.success(p))).join(', ')}`); + } + const generators = getGenerators(ctx, platforms); + const generated = await generateAssets(assets, generators, ctx.project); + if (!ctx.args.silent) { + logGenerated(generated); + } + /* + if (!ctx.args.silent && platforms.indexOf('pwa') >= 0 && ctx.args.pwaTags) { + PwaAssetGenerator.logInstructions(generated); + } + */ + return generated; + } + else { + log_1.logger.warn('No platforms found, exiting'); + return []; + } + } + catch (e) { + (0, log_1.error)('Unable to generate assets', e.message); + (0, log_1.error)(e); + } + return []; +} +exports.run = run; +async function verifyPlatformFolders(platforms, project) { + var _a, _b; + if (platforms.indexOf('ios') >= 0 && !(await project.iosExists())) { + platforms.splice(platforms.indexOf('ios'), 1); + log_1.logger.warn(`iOS platform not found at ${((_a = project.config.ios) === null || _a === void 0 ? void 0 : _a.path) || ''}, skipping iOS generation`); + } + if (platforms.indexOf('android') >= 0 && !(await project.androidExists())) { + platforms.splice(platforms.indexOf('android'), 1); + log_1.logger.warn(`Android platform not found at ${((_b = project.config.android) === null || _b === void 0 ? void 0 : _b.path) || ''}, skipping android generation`); + } +} +async function generateAssets(assets, generators, project) { + const generated = []; + async function generateAndCollect(asset) { + const g = await Promise.all(generators.map((g) => asset.generate(g, project))); + generated.push(...g.flat().filter((f) => !!f)); + } + const assetTypes = Object.values(assets).filter((v) => !!v); + for (const asset of assetTypes) { + await generateAndCollect(asset); + } + return generated; +} +function getGenerators(ctx, platforms) { + return platforms.map((p) => { + if (p === 'ios') { + return new ios_1.IosAssetGenerator(ctx.args); + } + if (p === 'android') { + return new android_1.AndroidAssetGenerator(ctx.args); + } + if (p === 'pwa') { + return new pwa_1.PwaAssetGenerator(ctx.args); + } + }); +} +// Print out a nice report of the assets generated +// and totals per platform +function logGenerated(generated) { + const sorted = generated.slice().sort((a, b) => { + return a.template.platform.localeCompare(b.template.platform); + }); + for (const g of sorted) { + Object.keys(g.destFilenames).forEach((name) => { + const filename = g.getDestFilename(name); + const outputInfo = g.getOutputInfo(name); + (0, log_1.log)(`${c.strong(c.success('CREATE'))} ${c.strong(c.extra(g.template.platform))} ${c.weak(g.template.kind)} ${filename !== null && filename !== void 0 ? filename : ''}${outputInfo ? ` (${size(outputInfo.size)})` : ''}`); + }); + } + (0, log_1.log)('\n'); + // Aggregate total assets and size per platform + const totals = sorted.reduce((totals, g) => { + if (!(g.template.platform in totals)) { + totals[g.template.platform] = { + count: 0, + size: 0, + }; + } + const entry = totals[g.template.platform]; + const count = Object.values(g.destFilenames).reduce((v) => v + 1, 0); + const size = Object.values(g.outputInfoMap).reduce((v, c) => v + c.size, 0); + totals[g.template.platform] = { + count: entry.count + count, + size: entry.size + size, + }; + return totals; + }, {}); + (0, log_1.log)('Totals:'); + for (const platformName of Object.keys(totals).sort()) { + const e = totals[platformName]; + (0, log_1.log)(`${c.strong(c.success(platformName))}: ${c.strong(c.extra(e.count))} generated, ${c.strong(size(e.size))} total`); + } +} +function size(bytes) { + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Number((bytes / Math.pow(1024, i)).toFixed(2)) * 1 + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i]; +} diff --git a/dist/util/cli.d.ts b/dist/util/cli.d.ts new file mode 100644 index 0000000..4fd5c06 --- /dev/null +++ b/dist/util/cli.d.ts @@ -0,0 +1 @@ +export declare function wrapAction(action: any): (...args: any[]) => Promise; diff --git a/dist/util/cli.js b/dist/util/cli.js new file mode 100644 index 0000000..54f9f7a --- /dev/null +++ b/dist/util/cli.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.wrapAction = void 0; +const log_1 = require("./log"); +function wrapAction(action) { + return async (...args) => { + try { + await action(...args); + } + catch (e) { + log_1.logger.error(e.message); + throw e; + } + }; +} +exports.wrapAction = wrapAction; +/* +export async function logPrompt(msg: string, promptObject: any) { + const { wordWrap } = await import('@ionic/cli-framework-output'); + const prompt = await import('prompts'); + + logger.log({ + msg: `${c.input(`[?]`)} ${wordWrap(msg, { indentation: 4 })}`, + logger, + format: false, + }); + + return prompt.default(promptObject, { onCancel: () => process.exit(1) }); +} + +*/ diff --git a/dist/util/log.d.ts b/dist/util/log.d.ts new file mode 100644 index 0000000..375f632 --- /dev/null +++ b/dist/util/log.d.ts @@ -0,0 +1,8 @@ +import { StreamOutputStrategy } from '@ionic/cli-framework-output'; +export declare const output: StreamOutputStrategy; +export declare const logger: import("@ionic/cli-framework-output").Logger; +export declare function debug(...args: any[]): void; +export declare function log(...args: any[]): void; +export declare function warn(...args: any[]): void; +export declare function error(...args: any[]): void; +export declare function fatal(msg: string, exc?: Error): never; diff --git a/dist/util/log.js b/dist/util/log.js new file mode 100644 index 0000000..c6b0cfa --- /dev/null +++ b/dist/util/log.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.fatal = exports.error = exports.warn = exports.log = exports.debug = exports.logger = exports.output = void 0; +const tslib_1 = require("tslib"); +const cli_framework_output_1 = require("@ionic/cli-framework-output"); +const colors_1 = (0, tslib_1.__importDefault)(require("../colors")); +const term_1 = require("./term"); +const options = { + colors: colors_1.default, + stream: process.argv.includes('--json') ? process.stderr : process.stdout, +}; +exports.output = (0, term_1.isInteractive)() ? new cli_framework_output_1.TTYOutputStrategy(options) : new cli_framework_output_1.StreamOutputStrategy(options); +exports.logger = (0, cli_framework_output_1.createDefaultLogger)({ + output: exports.output, + formatterOptions: { + titleize: false, + tags: new Map([ + [cli_framework_output_1.LOGGER_LEVELS.DEBUG, colors_1.default.log.DEBUG('[debug]')], + [cli_framework_output_1.LOGGER_LEVELS.INFO, colors_1.default.log.INFO('[info]')], + [cli_framework_output_1.LOGGER_LEVELS.WARN, colors_1.default.log.WARN('[warn]')], + [cli_framework_output_1.LOGGER_LEVELS.ERROR, colors_1.default.log.ERROR('[error]')], + ]), + }, +}); +function debug(...args) { + if (process.env.VERBOSE !== 'false') { + console.log(...args); + } +} +exports.debug = debug; +function log(...args) { + console.log(...args); +} +exports.log = log; +function warn(...args) { + console.warn(...args); +} +exports.warn = warn; +function error(...args) { + console.error(...args); +} +exports.error = error; +function fatal(msg, exc) { + console.error(colors_1.default.failure(`Fatal error: ${msg}`)); + console.log('ERROR', msg, exc); + if (exc) { + console.error(exc); + } + process.exit(1); +} +exports.fatal = fatal; diff --git a/dist/util/subprocess.d.ts b/dist/util/subprocess.d.ts new file mode 100644 index 0000000..5edf2e1 --- /dev/null +++ b/dist/util/subprocess.d.ts @@ -0,0 +1 @@ +export declare function runCommand(command: string, args: string[], options?: {}): Promise; diff --git a/dist/util/subprocess.js b/dist/util/subprocess.js new file mode 100644 index 0000000..966f894 --- /dev/null +++ b/dist/util/subprocess.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runCommand = void 0; +const tslib_1 = require("tslib"); +const utils_subprocess_1 = require("@ionic/utils-subprocess"); +const colors_1 = (0, tslib_1.__importDefault)(require("../colors")); +async function runCommand(command, args, options = {}) { + console.log(colors_1.default.strong(`> ${command} ${args.join(' ')}`)); + const p = new utils_subprocess_1.Subprocess(command, args, options); + try { + // return await p.output(); + return await p.run(); + } + catch (e) { + if (e instanceof utils_subprocess_1.SubprocessError) { + // old behavior of just throwing the stdout/stderr strings + throw e.output ? e.output : e.code ? e.code : e.error ? e.error.message : 'Unknown error'; + } + throw e; + } +} +exports.runCommand = runCommand; diff --git a/dist/util/term.d.ts b/dist/util/term.d.ts new file mode 100644 index 0000000..7b41305 --- /dev/null +++ b/dist/util/term.d.ts @@ -0,0 +1,2 @@ +export declare const checkInteractive: (...args: any[]) => boolean; +export declare const isInteractive: () => boolean; diff --git a/dist/util/term.js b/dist/util/term.js new file mode 100644 index 0000000..7f727d4 --- /dev/null +++ b/dist/util/term.js @@ -0,0 +1,30 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isInteractive = exports.checkInteractive = void 0; +const tslib_1 = require("tslib"); +const utils_terminal_1 = require("@ionic/utils-terminal"); +const colors_1 = (0, tslib_1.__importDefault)(require("../colors")); +const log_1 = require("./log"); +// Given input variables to a command, make sure all are provided if the terminal +// is not interactive (because we won't be able to prompt the user) +const checkInteractive = (...args) => { + if ((0, exports.isInteractive)()) { + return true; + } + // Fail if no args are provided, treat this as just a check of whether the term is + // interactive or not. + if (!args.length) { + return false; + } + // Make sure none of the provided args are empty, otherwise print the interactive + // warning and return false + if (args.filter((arg) => !arg).length) { + log_1.logger.error(`Non-interactive shell detected.\n` + + `Run the command with ${colors_1.default.input('--help')} to see a list of arguments that must be provided.`); + return false; + } + return true; +}; +exports.checkInteractive = checkInteractive; +const isInteractive = () => utils_terminal_1.TERMINAL_INFO.tty && !utils_terminal_1.TERMINAL_INFO.ci; +exports.isInteractive = isInteractive; diff --git a/package.json b/package.json index 8962894..3e3faba 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "build": "tsc", "watch": "tsc -w", "test": "jest --maxWorkers=4", - "lint": "npm run eslint && npm run prettier -- --check", + "lint": "npm run eslint", "fmt": "npm run prettier -- --write", "eslint": "eslint . --ext ts", "prettier": "prettier 'src/**/*.ts'", @@ -102,4 +102,4 @@ "node": "16.13.2", "npm": "8.3.0" } -} +} \ No newline at end of file