Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract ShortcutInfo module #270

Merged
merged 2 commits into from
Aug 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/core/src/lib/ShortcutInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

import {findSuitableIcon} from './util';
import {WebManifestShortcutJson} from './types/WebManifest';

// As described on https://developer.chrome.com/apps/manifest/name#short_name
const SHORT_NAME_MAX_SIZE = 12;

// The minimum size needed for the shortcut icon
const MIN_SHORTCUT_ICON_SIZE = 96;

/**
* A wrapper around the WebManifest's ShortcutInfo.
*/
export class ShortcutInfo {
/**
* @param {string} name
* @param {string} shortName
* @param {string} url target Url for when the shortcut is clicked
* @param {string} chosenIconUrl Url for the icon
*/
constructor(readonly name: string, readonly shortName: string, readonly url: string,
readonly chosenIconUrl: string) {
}

toString(index: number): string {
return `[name:'${this.name}', short_name:'${this.shortName}', ` +
`url:'${this.url}', icon:'shortcut_${index}']`;
}

/**
* Creates a new TwaManifest, using the URL for the Manifest as a base URL and uses the content
* of the Web Manifest to generate the fields for the TWA Manifest.
*
* @param {URL} webManifestUrl the URL where the webmanifest is available.
* @param {WebManifest} webManifest the Web Manifest, used as a base for the TWA Manifest.
* @returns {TwaManifest}
*/
static fromShortcutJson(webManifestUrl: URL, shortcut: WebManifestShortcutJson): ShortcutInfo {
const name = shortcut.name || shortcut.short_name;

if (!shortcut.icons || !shortcut.url || !name) {
throw new Error('missing metadata');
}

const suitableIcon = findSuitableIcon(shortcut.icons, 'any', MIN_SHORTCUT_ICON_SIZE);
if (!suitableIcon) {
throw new Error('not finding a suitable icon');
}

const shortName = shortcut.short_name || shortcut.name!.substring(0, SHORT_NAME_MAX_SIZE);
const url = new URL(shortcut.url, webManifestUrl).toString();
const iconUrl = new URL(suitableIcon.src, webManifestUrl).toString();
const shortcutInfo = new ShortcutInfo(name!, shortName!, url, iconUrl);

return shortcutInfo;
}
}
5 changes: 3 additions & 2 deletions packages/core/src/lib/TwaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import * as Jimp from 'jimp';
import fetch from 'node-fetch';
import {template} from 'lodash';
import {promisify} from 'util';
import {TwaManifest, ShortcutInfo} from './TwaManifest';
import {TwaManifest} from './TwaManifest';
import {ShortcutInfo} from './ShortcutInfo';
import Log from './Log';

const COPY_FILE_LIST = [
Expand Down Expand Up @@ -187,7 +188,7 @@ export class TwaGenerator {

private async saveIcon(data: Buffer, size: number, fileName: string): Promise<void> {
const image = await Jimp.read(data);
await image.resize(size, size);
image.resize(size, size);
await image.writeAsync(fileName);
}

Expand Down
61 changes: 13 additions & 48 deletions packages/core/src/lib/TwaManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@ import {findSuitableIcon, generatePackageId, validateNotEmpty} from './util';
import Color = require('color');
import Log from './Log';
import {WebManifestIcon, WebManifestJson} from './types/WebManifest';
import {ShortcutInfo} from './ShortcutInfo';

// The minimum size needed for the app icon.
const MIN_ICON_SIZE = 512;

// As described on https://developer.chrome.com/apps/manifest/name#short_name
const SHORT_NAME_MAX_SIZE = 12;

// The minimum size needed for the shortcut icon
const MIN_SHORTCUT_ICON_SIZE = 96;

// The minimum size needed for the notification icon
const MIN_NOTIFICATION_ICON_SIZE = 48;

Expand Down Expand Up @@ -60,21 +58,6 @@ const DEFAULT_GENERATOR_APP_NAME = 'unknown';

export type FallbackType = 'customtabs' | 'webview';

/**
* A wrapper around the WebManifest's ShortcutInfo.
*/
export class ShortcutInfo {
/**
* @param {string} name
* @param {string} shortName
* @param {string} url target Url for when the shortcut is clicked
* @param {string} chosenIconUrl Url for the icon
*/
constructor(readonly name: string, readonly shortName: string, readonly url: string,
readonly chosenIconUrl: string) {
}
}

/**
* A Manifest used to generate the TWA Project
*
Expand Down Expand Up @@ -204,10 +187,7 @@ export class TwaManifest {
}

generateShortcuts(): string {
return '[' + this.shortcuts.map((s: ShortcutInfo, i: number) =>
`[name:'${s.name}', short_name:'${s.shortName}', url:'${s.url}', icon:'shortcut_${i}']`)
.join(',') +
']';
return '[' + this.shortcuts.map((shortcut, i) => shortcut.toString(i)).join(',') + ']';
}

/**
Expand All @@ -219,41 +199,26 @@ export class TwaManifest {
* @returns {TwaManifest}
*/
static fromWebManifestJson(webManifestUrl: URL, webManifest: WebManifestJson): TwaManifest {
const icon: WebManifestIcon | null = webManifest.icons ?
findSuitableIcon(webManifest.icons, 'any', MIN_ICON_SIZE) : null;

const maskableIcon: WebManifestIcon | null = webManifest.icons ?
findSuitableIcon(webManifest.icons, 'maskable', MIN_ICON_SIZE) : null;

const monochromeIcon: WebManifestIcon | null = webManifest.icons ?
findSuitableIcon(webManifest.icons, 'monochrome', MIN_NOTIFICATION_ICON_SIZE) : null;
const icon = findSuitableIcon(webManifest.icons, 'any', MIN_ICON_SIZE);
const maskableIcon = findSuitableIcon(webManifest.icons, 'maskable', MIN_ICON_SIZE);
const monochromeIcon =
findSuitableIcon(webManifest.icons, 'monochrome', MIN_NOTIFICATION_ICON_SIZE);

const fullStartUrl: URL = new URL(webManifest['start_url'] || '/', webManifestUrl);

const shortcuts: ShortcutInfo[] = [];

for (let i = 0; i < (webManifest.shortcuts || []).length; i++) {
const s = webManifest.shortcuts![i];

if (!s.icons || !s.url || (!s.name && !s.short_name)) {
TwaManifest.log.warn(`Skipping shortcut[${i}] for missing metadata.`);
continue;
try {
const shortcutInfo = ShortcutInfo.fromShortcutJson(webManifestUrl, s);
if (shortcutInfo != null) {
shortcuts.push(shortcutInfo);
}
} catch (err) {
TwaManifest.log.warn(`Skipping shortcut[${i}] for ${err.message}.`);
}

const suitableIcon = findSuitableIcon(s.icons, 'any', MIN_SHORTCUT_ICON_SIZE);
if (!suitableIcon) {
TwaManifest.log.warn(`Skipping shortcut[${i}] for not finding a suitable icon.`);
continue;
}

const name = s.name || s.short_name;
const shortName = s.short_name || s.name!.substring(0, SHORT_NAME_MAX_SIZE);
const url = new URL(s.url, webManifestUrl).toString();
const iconUrl = new URL(suitableIcon.src, webManifestUrl).toString();
const shortcutInfo = new ShortcutInfo(name!, shortName!, url, iconUrl);

shortcuts.push(shortcutInfo);

if (shortcuts.length === 4) {
break;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ export function execInteractive(
* @param {number} minSize The minimum required icon size enforced id provided.
*/
export function findSuitableIcon(
icons: WebManifestIcon[], purpose: string, minSize = 0): WebManifestIcon | null {
if (icons.length === 0) {
icons: WebManifestIcon[] | undefined, purpose: string, minSize = 0): WebManifestIcon | null {
if (icons == undefined || icons.length === 0) {
return null;
}

Expand Down
79 changes: 79 additions & 0 deletions packages/core/src/spec/lib/ShortcutInfoSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {ShortcutInfo} from '../../lib/ShortcutInfo';

describe('ShortcutInfo', () => {
describe('#fromShortcutJson', () => {
it('creates a correct TWA shortcut', () => {
const shortcut = {
'name': 'shortcut name',
'short_name': 'short',
'url': '/launch',
'icons': [{
'src': '/shortcut_icon.png',
'sizes': '96x96',
}],
};
const manifestUrl = new URL('https://pwa-directory.com/manifest.json');
const shortcutInfo = ShortcutInfo.fromShortcutJson(manifestUrl, shortcut);
expect(shortcutInfo.name).toBe('shortcut name');
expect(shortcutInfo.shortName).toBe('short');
expect(shortcutInfo.url).toBe('https://pwa-directory.com/launch');
expect(shortcutInfo.chosenIconUrl)
.toBe('https://pwa-directory.com/shortcut_icon.png');
expect(shortcutInfo.toString(0))
.toBe('[name:\'shortcut name\', short_name:\'short\',' +
' url:\'https://pwa-directory.com/launch\', icon:\'shortcut_0\']');
});

it('Throws if icon size is empty or too small', () => {
const shortcut = {
'name': 'invalid',
'url': '/invalid',
'icons': [{
'src': '/no_size.png',
}, {
'src': '/small_size.png',
'sizes': '95x95',
}],
};
const manifestUrl = new URL('https://pwa-directory.com/manifest.json');
expect(() => ShortcutInfo.fromShortcutJson(manifestUrl, shortcut))
.toThrowError('not finding a suitable icon');
});

it('Throws if icons is missing', () => {
const shortcut = {
'name': 'invalid',
'url': '/invalid',
};
const manifestUrl = new URL('https://pwa-directory.com/manifest.json');
expect(() => ShortcutInfo.fromShortcutJson(manifestUrl, shortcut))
.toThrowError('missing metadata');
});
});

describe('#constructor', () => {
it('Builds a ShortcutInfo correctly', () => {
const shortcutInfo = new ShortcutInfo('name', 'shortName', '/', 'icon.png');
expect(shortcutInfo.name).toEqual('name');
expect(shortcutInfo.shortName).toEqual('shortName');
expect(shortcutInfo.url).toEqual('/');
expect(shortcutInfo.chosenIconUrl).toEqual('icon.png');
});
});
});
4 changes: 4 additions & 0 deletions packages/core/src/spec/lib/utilSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ describe('util', () => {
expect(result).toBeNull();
});

it('returns null for an undefined icon list', () => {
expect(util.findSuitableIcon(undefined, 'any')).toBeNull();
});

it('Ignores SVG Icons by mime-type', () => {
const result = util.findSuitableIcon(
[{
Expand Down