import { HTMLElement } from "node-html-parser"; import { CheckerMessage, CheckerStatus, Fetcher, MessageId, WebAppManifestReport } from "./types"; import { CheckIconOutput, CheckIconProcessor, checkIcon, fetchFetcher, mergeUrlAndPath, pathToMimeType } from "./helper"; export const checkWebAppManifest = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise => { const messages: CheckerMessage[] = []; let name = undefined; let shortName = undefined; let backgroundColor = undefined; let themeColor = undefined; let icon = null; if (!head) { messages.push({ status: CheckerStatus.Error, id: MessageId.noHead, text: 'No element' }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } const manifestMarkup = head.querySelectorAll("link[rel='manifest']"); if (manifestMarkup.length === 0) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifest, text: 'No web app manifest' }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } const href = manifestMarkup[0].getAttribute('href'); if (!href) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestHref, text: 'The web app manifest markup has no `href` attribute' }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } const manifestUrl = mergeUrlAndPath(baseUrl, href); const manifest = await fetcher(manifestUrl, 'application/json'); if (manifest.status === 404) { messages.push({ status: CheckerStatus.Error, id: MessageId.manifest404, text: `The web app manifest at \`${href}\` is not found` }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } else if (manifest.status >= 300 || !manifest.readableStream) { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestCannotGet, text: `Cannot get the web app manifest at \`${href}\` (${manifest.status} error)` }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } let parsedManifest; try { parsedManifest = await readableStreamToJson(manifest.readableStream); } catch(e) { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestInvalidJson, text: `Cannot parse the web app manifest at \`${href}\`` }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } const manifestReport = await checkWebAppManifestFile(parsedManifest, manifestUrl, fetcher); return { ...manifestReport, messages: messages.concat(manifestReport.messages), } } const readableStreamToJson = async (stream: ReadableStream): Promise => { const reader = stream.getReader(); const decoder = new TextDecoder(); let result = ''; while (true) { const { done, value } = await reader.read(); if (done) { break; } result += decoder.decode(value); } return JSON.parse(result); } export const checkWebAppManifestFile = async (manifest: any, baseUrl: string, fetcher: Fetcher): Promise => { const messages: CheckerMessage[] = []; let icon: CheckIconOutput | null = null; const name = manifest.name || undefined; if (!name) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestName, text: 'The web app manifest has no `name`' }); } else { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestName, text: `The web app manifest has the name "${name}"` }); } const shortName = manifest.short_name || undefined; if (!shortName) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestShortName, text: 'The web app manifest has no `short_name`' }); } else { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestShortName, text: `The web app manifest has the short name "${shortName}"` }); } const backgroundColor = manifest.background_color || undefined; if (!backgroundColor) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestBackgroundColor, text: 'The web app manifest has no `background_color`' }); } else { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestBackgroundColor, text: `The web app manifest has the background color \`${backgroundColor}\`` }); } const themeColor = manifest.theme_color || undefined; if (!themeColor) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestThemeColor, text: 'The web app manifest has no `theme_color`' }); } else { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestThemeColor, text: `The web app manifest has the theme color \`${themeColor}\`` }); } const icons = manifest.icons; if (!icons || !Array.isArray(icons) || icons.length === 0) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestIcons, text: 'The web app manifest has no `icons`' }); return { messages, name, shortName, backgroundColor, themeColor, icon }; } for await (const size of [ 192, 512 ]) { const iconEntry = icons.find((icon: any) => icon.sizes === `${size}x${size}`); if (!iconEntry) { messages.push({ status: CheckerStatus.Error, id: MessageId.noManifestIcon, text: `The web app manifest has no ${size}x${size} icon` }); } else { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestIconDeclared, text: `The web app manifest has a ${size}x${size} icon` }); const iconUrl = mergeUrlAndPath(baseUrl, iconEntry.src); const processor: CheckIconProcessor = { cannotGet: (status) => { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestIconCannotGet, text: `The ${size}x${size} icon cannot be fetched (${status})` }); }, downloadable: () => { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestIconDownloadable, text: `The ${size}x${size} icon is downloadable` }); }, icon404: () => { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestIcon404, text: `The ${size}x${size} icon is not found` }); }, noHref: () => { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestIconNoHref, text: `The ${size}x${size} icon has no \`href\` attribute` }); }, notSquare: () => { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestIconNotSquare, text: `The ${size}x${size} icon is not square` }); }, rightSize: () => { messages.push({ status: CheckerStatus.Ok, id: MessageId.manifestIconRightSize, text: `The ${size}x${size} icon has the right size` }); }, square: () => { // Ignore this, just check the size }, wrongSize: (actualSize) => { messages.push({ status: CheckerStatus.Error, id: MessageId.manifestIconWrongSize, text: `The ${size}x${size} icon has the wrong size (${actualSize})` }); } }; icon = await checkIcon( iconUrl, processor, fetcher, iconEntry.type || pathToMimeType(iconEntry.src), size ); } } return { messages, name, shortName, backgroundColor, themeColor, icon }; }