@realfavicongenerator/check-favicon 0.4.7 → 0.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/desktop/desktop.d.ts +4 -4
- package/dist/desktop/desktop.js +40 -14
- package/dist/desktop/desktop.test.js +149 -64
- package/dist/desktop/ico.d.ts +2 -2
- package/dist/desktop/ico.js +30 -6
- package/dist/google.d.ts +8 -0
- package/dist/google.js +107 -0
- package/dist/google.test.js +71 -0
- package/dist/helper.d.ts +8 -1
- package/dist/helper.js +26 -7
- package/dist/helper.test.js +18 -3
- package/dist/touch-icon.js +1 -1
- package/dist/types.d.ts +29 -1
- package/dist/types.js +8 -0
- package/dist/web-app-manifest.js +1 -1
- package/package.json +6 -4
- package/src/desktop/desktop.test.ts +152 -64
- package/src/desktop/desktop.ts +46 -19
- package/src/desktop/ico.ts +35 -10
- package/src/google.test.ts +82 -0
- package/src/google.ts +103 -0
- package/src/helper.test.ts +19 -4
- package/src/helper.ts +35 -7
- package/src/touch-icon.ts +3 -3
- package/src/types.ts +33 -1
- package/src/web-app-manifest.ts +3 -3
- package/dist/web-manifest.d.ts +0 -4
- package/dist/web-manifest.js +0 -262
- package/dist/web-manifest.test.js +0 -172
- /package/dist/{web-manifest.test.d.ts → google.test.d.ts} +0 -0
package/src/desktop/desktop.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { CheckerMessage, CheckerStatus, DesktopFaviconReport, Fetcher, MessageId } from "../types";
|
|
1
|
+
import { CheckerMessage, CheckerStatus, DesktopFaviconReport, DesktopSingleReport, Fetcher, MessageId } from "../types";
|
|
2
2
|
import { HTMLElement } from 'node-html-parser'
|
|
3
3
|
import sharp from 'sharp'
|
|
4
|
-
import { CheckIconProcessor, checkIcon, fetchFetcher, mergeUrlAndPath, readableStreamToString } from "../helper";
|
|
4
|
+
import { CheckIconProcessor, bufferToDataUrl, checkIcon, fetchFetcher, mergeUrlAndPath, readableStreamToString } from "../helper";
|
|
5
5
|
import { checkIcoFavicon } from "./ico";
|
|
6
6
|
|
|
7
7
|
export const PngFaviconFileSize = 96;
|
|
8
8
|
|
|
9
|
-
export const checkSvgFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<
|
|
9
|
+
export const checkSvgFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<DesktopSingleReport> => {
|
|
10
10
|
const messages: CheckerMessage[] = [];
|
|
11
11
|
|
|
12
12
|
if (!head) {
|
|
@@ -16,7 +16,10 @@ export const checkSvgFavicon = async (baseUrl: string, head: HTMLElement | null,
|
|
|
16
16
|
text: 'No <head> element'
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
messages,
|
|
21
|
+
icon: { content: null, url: null, width: null, height: null }
|
|
22
|
+
};
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
const svgs = head?.querySelectorAll("link[rel='icon'][type='image/svg+xml']");
|
|
@@ -47,20 +50,29 @@ export const checkSvgFavicon = async (baseUrl: string, head: HTMLElement | null,
|
|
|
47
50
|
text: 'The SVG markup has no href attribute'
|
|
48
51
|
});
|
|
49
52
|
} else {
|
|
50
|
-
const
|
|
51
|
-
return
|
|
53
|
+
const iconReport = await checkSvgFaviconFile(baseUrl, href, fetcher)
|
|
54
|
+
return {
|
|
55
|
+
messages: [ ...messages, ...iconReport.messages ],
|
|
56
|
+
icon: iconReport.icon
|
|
57
|
+
};
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
return
|
|
61
|
+
return {
|
|
62
|
+
messages,
|
|
63
|
+
icon: { content: null, url: null, width: null, height: null }
|
|
64
|
+
};
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
export const checkSvgFaviconFile = async (baseUrl: string, url: string, fetcher: Fetcher): Promise<
|
|
67
|
+
export const checkSvgFaviconFile = async (baseUrl: string, url: string, fetcher: Fetcher): Promise<DesktopSingleReport> => {
|
|
59
68
|
const messages: CheckerMessage[] = [];
|
|
60
69
|
|
|
61
70
|
const svgUrl = mergeUrlAndPath(baseUrl, url);
|
|
62
71
|
|
|
63
72
|
const res = await fetcher(svgUrl, 'image/svg+xml');
|
|
73
|
+
let content;
|
|
74
|
+
let width: number | null = null;
|
|
75
|
+
let height: number | null = null;
|
|
64
76
|
if (res.status === 404) {
|
|
65
77
|
messages.push({
|
|
66
78
|
status: CheckerStatus.Error,
|
|
@@ -80,28 +92,37 @@ export const checkSvgFaviconFile = async (baseUrl: string, url: string, fetcher:
|
|
|
80
92
|
text: `The SVG favicon is accessible at \`${url}\``
|
|
81
93
|
});
|
|
82
94
|
|
|
83
|
-
|
|
95
|
+
content = await readableStreamToString(res.readableStream);
|
|
84
96
|
const meta = await sharp(Buffer.from(content)).metadata();
|
|
97
|
+
width = meta.width || null;
|
|
98
|
+
height = meta.height || null;
|
|
85
99
|
|
|
86
|
-
if (
|
|
100
|
+
if (width && height && width !== height) {
|
|
87
101
|
messages.push({
|
|
88
102
|
status: CheckerStatus.Error,
|
|
89
103
|
id: MessageId.svgFaviconNotSquare,
|
|
90
|
-
text: `The SVG is not square (${
|
|
104
|
+
text: `The SVG is not square (${width}x${height})`
|
|
91
105
|
});
|
|
92
106
|
} else {
|
|
93
107
|
messages.push({
|
|
94
108
|
status: CheckerStatus.Ok,
|
|
95
109
|
id: MessageId.svgFaviconSquare,
|
|
96
|
-
text: `The SVG is square (${
|
|
110
|
+
text: `The SVG is square (${width}x${height})`
|
|
97
111
|
});
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
return
|
|
115
|
+
return {
|
|
116
|
+
messages,
|
|
117
|
+
icon: {
|
|
118
|
+
content: content ? await bufferToDataUrl(Buffer.from(content), 'image/svg+xml') : null,
|
|
119
|
+
url: svgUrl,
|
|
120
|
+
width, height
|
|
121
|
+
}
|
|
122
|
+
};
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
export const checkPngFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<
|
|
125
|
+
export const checkPngFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<DesktopSingleReport> => {
|
|
105
126
|
const messages: CheckerMessage[] = [];
|
|
106
127
|
|
|
107
128
|
if (!head) {
|
|
@@ -111,7 +132,7 @@ export const checkPngFavicon = async (baseUrl: string, head: HTMLElement | null,
|
|
|
111
132
|
text: 'No <head> element'
|
|
112
133
|
});
|
|
113
134
|
|
|
114
|
-
return { messages, icon: null };
|
|
135
|
+
return { messages, icon: { content: null, url: null, width: null, height: null } };
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
const icons = head?.querySelectorAll("link[rel='icon'][type='image/png']");
|
|
@@ -193,15 +214,21 @@ export const checkPngFavicon = async (baseUrl: string, head: HTMLElement | null,
|
|
|
193
214
|
}
|
|
194
215
|
}
|
|
195
216
|
|
|
196
|
-
return { messages, icon: null };
|
|
217
|
+
return { messages, icon: { content: null, url: null, width: null, height: null } };
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
export const checkDesktopFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<DesktopFaviconReport> => {
|
|
200
|
-
const
|
|
221
|
+
const svgReport = await checkSvgFavicon(baseUrl, head, fetcher);
|
|
201
222
|
const pngReport = await checkPngFavicon(baseUrl, head, fetcher);
|
|
202
223
|
const icoReport = await checkIcoFavicon(baseUrl, head, fetcher);
|
|
224
|
+
|
|
203
225
|
return {
|
|
204
|
-
messages: [ ...
|
|
205
|
-
icon: pngReport.icon
|
|
226
|
+
messages: [ ...svgReport.messages, ...pngReport.messages, ...icoReport.messages ],
|
|
227
|
+
icon: pngReport.icon ? pngReport.icon.content : null,
|
|
228
|
+
icons: {
|
|
229
|
+
png: pngReport.icon,
|
|
230
|
+
ico: icoReport.icon,
|
|
231
|
+
svg: svgReport.icon
|
|
232
|
+
}
|
|
206
233
|
};
|
|
207
234
|
}
|
package/src/desktop/ico.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { CheckerMessage, CheckerStatus, Fetcher, MessageId } from "../types";
|
|
1
|
+
import { CheckedIcon, CheckerMessage, CheckerStatus, DesktopSingleReport, Fetcher, MessageId } from "../types";
|
|
2
2
|
import { HTMLElement } from 'node-html-parser'
|
|
3
|
-
import { mergeUrlAndPath, readableStreamToBuffer } from "../helper";
|
|
3
|
+
import { bufferToDataUrl, mergeUrlAndPath, readableStreamToBuffer } from "../helper";
|
|
4
4
|
import decodeIco from "decode-ico";
|
|
5
5
|
|
|
6
6
|
export const IcoFaviconSizes = [ 48, 32, 16 ];
|
|
7
7
|
|
|
8
|
-
export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fetcher: Fetcher): Promise<
|
|
8
|
+
export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fetcher: Fetcher): Promise<DesktopSingleReport> => {
|
|
9
9
|
const messages: CheckerMessage[] = [];
|
|
10
10
|
|
|
11
11
|
if (!head) {
|
|
@@ -15,12 +15,19 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
|
|
|
15
15
|
text: 'No <head> element'
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
return
|
|
18
|
+
return {
|
|
19
|
+
messages,
|
|
20
|
+
icon : { content: null, url: null, width: null, height: null }
|
|
21
|
+
};
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
const icos =
|
|
22
|
-
head.querySelectorAll('link[rel="shortcut icon"]')
|
|
23
|
-
head.querySelectorAll('link[rel="icon"][type="image/x-icon"]')
|
|
24
|
+
const icos = [
|
|
25
|
+
...head.querySelectorAll('link[rel="shortcut icon"]'),
|
|
26
|
+
...head.querySelectorAll('link[rel="icon"][type="image/x-icon"]')
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let iconUrl: string | null = null;
|
|
30
|
+
let images;
|
|
24
31
|
|
|
25
32
|
if (icos.length === 0) {
|
|
26
33
|
messages.push({
|
|
@@ -49,7 +56,7 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
|
|
|
49
56
|
text: 'The ICO markup has no href attribute'
|
|
50
57
|
});
|
|
51
58
|
} else {
|
|
52
|
-
|
|
59
|
+
iconUrl = mergeUrlAndPath(url, href);
|
|
53
60
|
const iconResponse = await fetcher(iconUrl, 'image/x-icon');
|
|
54
61
|
if (iconResponse.status === 404) {
|
|
55
62
|
messages.push({
|
|
@@ -71,7 +78,7 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
|
|
|
71
78
|
});
|
|
72
79
|
|
|
73
80
|
const iconBuffer = await readableStreamToBuffer(iconResponse.readableStream);
|
|
74
|
-
|
|
81
|
+
images = await decodeIco(iconBuffer);
|
|
75
82
|
|
|
76
83
|
const imageSizes = images.map(image => `${image.width}x${image.height}`);
|
|
77
84
|
|
|
@@ -106,5 +113,23 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
|
|
116
|
+
let content: string | null = null;
|
|
117
|
+
const theIcon: CheckedIcon = {
|
|
118
|
+
content: null,
|
|
119
|
+
url: iconUrl,
|
|
120
|
+
width: null,
|
|
121
|
+
height: null
|
|
122
|
+
};
|
|
123
|
+
if (images) {
|
|
124
|
+
const image = images[0];
|
|
125
|
+
const mimeType = (image.type === "bmp") ? "image/bmp" : "image/png";
|
|
126
|
+
theIcon.content = await bufferToDataUrl(Buffer.from(image.data), mimeType);
|
|
127
|
+
theIcon.width = image.width;
|
|
128
|
+
theIcon.height = image.height;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
messages,
|
|
133
|
+
icon: theIcon,
|
|
134
|
+
};
|
|
110
135
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { checkRobotsFile, getRobotsFileUrl } from "./google";
|
|
2
|
+
import { stringToReadableStream } from "./helper";
|
|
3
|
+
import { testFetcher } from "./test-helper";
|
|
4
|
+
import { CheckerMessage, CheckerStatus, DesktopFaviconReport, MessageId } from "./types";
|
|
5
|
+
|
|
6
|
+
test('getRobotsFileUrl', () => {
|
|
7
|
+
expect(getRobotsFileUrl('https://example.com')).toEqual('https://example.com/robots.txt');
|
|
8
|
+
expect(getRobotsFileUrl('https://example.com/some-path')).toEqual('https://example.com/robots.txt');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const runRobotsTest = async (urls: string[], robotsFile: string | null, messages: Pick<CheckerMessage, 'id' | 'status'>[]) => {
|
|
12
|
+
const report = await checkRobotsFile(
|
|
13
|
+
'https://example.com',
|
|
14
|
+
urls,
|
|
15
|
+
testFetcher(robotsFile ? {
|
|
16
|
+
'https://example.com/robots.txt': {
|
|
17
|
+
status: 200,
|
|
18
|
+
contentType: 'text/plain',
|
|
19
|
+
readableStream: await stringToReadableStream(robotsFile)
|
|
20
|
+
}
|
|
21
|
+
} : {})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const filteredMessages = report.map(m => ({ status: m.status, id: m.id }));
|
|
25
|
+
expect(filteredMessages).toEqual(messages);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('checkRobotsFile - No robots file', async () => {
|
|
29
|
+
await runRobotsTest(
|
|
30
|
+
[ 'https://example.com/favicon.png' ],
|
|
31
|
+
null,
|
|
32
|
+
[
|
|
33
|
+
{
|
|
34
|
+
status: CheckerStatus.Ok,
|
|
35
|
+
id: MessageId.googleNoRobotsFile
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('checkRobotsFile - PNG favicon is accessible', async () => {
|
|
42
|
+
await runRobotsTest(
|
|
43
|
+
[ 'https://example.com/favicon.png' ],
|
|
44
|
+
`
|
|
45
|
+
User-agent: *
|
|
46
|
+
Allow: /`,
|
|
47
|
+
[
|
|
48
|
+
{
|
|
49
|
+
status: CheckerStatus.Ok,
|
|
50
|
+
id: MessageId.googleRobotsFileFound
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
status: CheckerStatus.Ok,
|
|
54
|
+
id: MessageId.googlePngIconAllowedByRobots
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('checkRobotsFile - PNG favicon is *not* accessible', async () => {
|
|
61
|
+
await runRobotsTest(
|
|
62
|
+
[ 'https://example.com/favicon.png' ],
|
|
63
|
+
`
|
|
64
|
+
# *
|
|
65
|
+
User-agent: *
|
|
66
|
+
Allow: /
|
|
67
|
+
|
|
68
|
+
User-agent: Googlebot-Image
|
|
69
|
+
Disallow: /*.png
|
|
70
|
+
`,
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
status: CheckerStatus.Ok,
|
|
74
|
+
id: MessageId.googleRobotsFileFound
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
status: CheckerStatus.Error,
|
|
78
|
+
id: MessageId.googlePngIconBlockedByRobots
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
);
|
|
82
|
+
});
|
package/src/google.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import robotsParser from "robots-parser";
|
|
2
|
+
import { checkDesktopFavicon } from "./desktop/desktop";
|
|
3
|
+
import { fetchFetcher, readableStreamToBuffer, readableStreamToString } from "./helper";
|
|
4
|
+
import { CheckedIcon, CheckerMessage, CheckerStatus, DesktopFaviconReport, Fetcher, GoogleReport, MessageId } from "./types";
|
|
5
|
+
import { HTMLElement } from "node-html-parser";
|
|
6
|
+
|
|
7
|
+
export const GoogleBot = 'Googlebot';
|
|
8
|
+
export const GoogleImageBot = 'Googlebot-Image';
|
|
9
|
+
|
|
10
|
+
export const getRobotsFileUrl = (baseUrl: string): string => {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(baseUrl);
|
|
13
|
+
url.pathname = '/robots.txt';
|
|
14
|
+
return url.toString();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error(`Invalid URL ${baseUrl}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const checkRobotsFile = async (baseUrl: string, iconUrls: string[], fetcher: Fetcher = fetchFetcher): Promise<CheckerMessage[]> => {
|
|
21
|
+
const robotsUrl = getRobotsFileUrl(baseUrl);
|
|
22
|
+
const robotsResponse = await fetcher(robotsUrl);
|
|
23
|
+
|
|
24
|
+
const messages: CheckerMessage[] = [];
|
|
25
|
+
|
|
26
|
+
if (robotsResponse.status === 200) {
|
|
27
|
+
messages.push({
|
|
28
|
+
status: CheckerStatus.Ok,
|
|
29
|
+
text: `robots.txt file found at ${robotsUrl}`,
|
|
30
|
+
id: MessageId.googleRobotsFileFound
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const robotsFile = robotsResponse.readableStream ? await readableStreamToString(robotsResponse.readableStream) : '';
|
|
34
|
+
|
|
35
|
+
const robots = robotsParser(robotsUrl, robotsFile);
|
|
36
|
+
|
|
37
|
+
iconUrls.forEach(url => {
|
|
38
|
+
if (url) {
|
|
39
|
+
if (robots.isAllowed(url, GoogleImageBot)) {
|
|
40
|
+
messages.push({
|
|
41
|
+
status: CheckerStatus.Ok,
|
|
42
|
+
text: `Access to \`${url}\` is allowed for \`${GoogleImageBot}\``,
|
|
43
|
+
id: MessageId.googlePngIconAllowedByRobots
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
const line = robots.getMatchingLineNumber(url, GoogleImageBot);
|
|
47
|
+
messages.push({
|
|
48
|
+
status: CheckerStatus.Error,
|
|
49
|
+
text: `Access to \`${url}\` is blocked for \`${GoogleImageBot}\` (\`${robotsUrl}\`, line ${line})`,
|
|
50
|
+
id: MessageId.googlePngIconBlockedByRobots
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
messages.push({
|
|
57
|
+
status: CheckerStatus.Ok,
|
|
58
|
+
text: `No \`robots.txt\` file found at \`${robotsUrl}\`. Also this is not a recommanded setup, at least Google is not restricted from accessing favicon assets.`,
|
|
59
|
+
id: MessageId.googleNoRobotsFile
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return messages;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const checkGoogleFaviconFromDesktopReport = async (baseUrl: string, desktopReport: DesktopFaviconReport, fetcher: Fetcher = fetchFetcher): Promise<GoogleReport> => {
|
|
67
|
+
const allIcons: CheckedIcon[] = [
|
|
68
|
+
desktopReport.icons.png,
|
|
69
|
+
desktopReport.icons.ico,
|
|
70
|
+
desktopReport.icons.svg
|
|
71
|
+
].filter((i): i is CheckedIcon => !!i);
|
|
72
|
+
|
|
73
|
+
const allIconUrls: string[] = allIcons.map(i => i.url).filter((i): i is string => !!i);
|
|
74
|
+
|
|
75
|
+
const robotsMessages = await checkRobotsFile(baseUrl, allIconUrls, fetcher);
|
|
76
|
+
|
|
77
|
+
const messages: CheckerMessage[] = [ ...desktopReport.messages, ...robotsMessages ];
|
|
78
|
+
|
|
79
|
+
let finalIcon: string | null = null;
|
|
80
|
+
let icons: CheckedIcon[] = [];
|
|
81
|
+
let maxWidth = 0;
|
|
82
|
+
|
|
83
|
+
allIcons.forEach(icon => {
|
|
84
|
+
if (icon.content && icon.width && icon.height && icon.url) {
|
|
85
|
+
icons.push(icon);
|
|
86
|
+
if (icon.width > maxWidth) {
|
|
87
|
+
finalIcon = icon.content;
|
|
88
|
+
maxWidth = icon.width;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
messages,
|
|
95
|
+
icon: finalIcon,
|
|
96
|
+
icons
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const checkGoogleFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<GoogleReport> => {
|
|
101
|
+
const desktopReport = await checkDesktopFavicon(baseUrl, head, fetcher);
|
|
102
|
+
return checkGoogleFaviconFromDesktopReport(baseUrl, desktopReport, fetcher);
|
|
103
|
+
}
|
package/src/helper.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import sharp from "sharp";
|
|
2
|
-
import { CheckIconProcessor, checkIcon, filePathToReadableStream, mergeUrlAndPath, parseSizesAttribute } from "./helper";
|
|
2
|
+
import { CheckIconProcessor, bufferToDataUrl, checkIcon, filePathToDataUrl, filePathToReadableStream, filePathToString, mergeUrlAndPath, parseSizesAttribute } from "./helper";
|
|
3
3
|
import { testFetcher } from "./test-helper";
|
|
4
4
|
|
|
5
5
|
const getTestProcessor = () => {
|
|
@@ -30,7 +30,12 @@ test('checkIcon - noHref', async () => {
|
|
|
30
30
|
|
|
31
31
|
test('checkIcon - icon404', async () => {
|
|
32
32
|
const processor = getTestProcessor();
|
|
33
|
-
expect(await checkIcon('/does-not-exist.png', processor.processor, testFetcher({}), 'image/png')).
|
|
33
|
+
expect(await checkIcon('/does-not-exist.png', processor.processor, testFetcher({}), 'image/png')).toEqual({
|
|
34
|
+
content: null,
|
|
35
|
+
url: '/does-not-exist.png',
|
|
36
|
+
width: null,
|
|
37
|
+
height: null
|
|
38
|
+
});
|
|
34
39
|
expect(processor.messages).toEqual(['icon404']);
|
|
35
40
|
})
|
|
36
41
|
|
|
@@ -41,7 +46,12 @@ test('checkIcon - icon404', async () => {
|
|
|
41
46
|
contentType: 'image/png',
|
|
42
47
|
status: 500
|
|
43
48
|
}
|
|
44
|
-
}), 'image/png')).
|
|
49
|
+
}), 'image/png')).toEqual({
|
|
50
|
+
content: null,
|
|
51
|
+
url: '/bad-icon.png',
|
|
52
|
+
width: null,
|
|
53
|
+
height: null
|
|
54
|
+
});
|
|
45
55
|
expect(processor.messages).toEqual(['cannotGet 500']);
|
|
46
56
|
})
|
|
47
57
|
|
|
@@ -100,7 +110,12 @@ test('checkIcon - downloadable & notSquare', async () => {
|
|
|
100
110
|
contentType: 'image/png',
|
|
101
111
|
readableStream: await filePathToReadableStream(nonSquareIcon)
|
|
102
112
|
}
|
|
103
|
-
}), 'image/png', 500)).
|
|
113
|
+
}), 'image/png', 500)).toEqual({
|
|
114
|
+
content: await filePathToDataUrl(nonSquareIcon),
|
|
115
|
+
url: '/non-square-icon.png',
|
|
116
|
+
width: 240,
|
|
117
|
+
height: 180
|
|
118
|
+
});
|
|
104
119
|
expect(processor.messages).toEqual([
|
|
105
120
|
'downloadable',
|
|
106
121
|
'notSquare 240x180'
|
package/src/helper.ts
CHANGED
|
@@ -24,9 +24,12 @@ export const filePathToString = async (path: string): Promise<string> => (
|
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
export const stringToReadableStream = (str: string): ReadableStream => {
|
|
27
|
+
const encoder = new TextEncoder();
|
|
28
|
+
const uint8Array = encoder.encode(str);
|
|
29
|
+
|
|
27
30
|
return new ReadableStream({
|
|
28
31
|
start(controller) {
|
|
29
|
-
controller.enqueue(
|
|
32
|
+
controller.enqueue(uint8Array);
|
|
30
33
|
controller.close();
|
|
31
34
|
}
|
|
32
35
|
});
|
|
@@ -100,13 +103,20 @@ export const pathToMimeType = (path: string): string => {
|
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
export type CheckIconOutput = {
|
|
107
|
+
content: string | null,
|
|
108
|
+
url: string | null,
|
|
109
|
+
width: number | null,
|
|
110
|
+
height: number | null,
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
export const checkIcon = async (
|
|
104
114
|
iconUrl: string | undefined,
|
|
105
115
|
processor: CheckIconProcessor,
|
|
106
116
|
fetcher: Fetcher,
|
|
107
117
|
mimeType: string | undefined,
|
|
108
118
|
expectedWidthHeight?: number
|
|
109
|
-
): Promise<
|
|
119
|
+
): Promise<CheckIconOutput | null> => {
|
|
110
120
|
if (!iconUrl) {
|
|
111
121
|
processor.noHref();
|
|
112
122
|
return null;
|
|
@@ -120,15 +130,16 @@ export const checkIcon = async (
|
|
|
120
130
|
} else if (res.readableStream) {
|
|
121
131
|
processor.downloadable();
|
|
122
132
|
|
|
123
|
-
const
|
|
124
|
-
const meta = await sharp(
|
|
133
|
+
const rawContent = await readableStreamToBuffer(res.readableStream);
|
|
134
|
+
const meta = await sharp(rawContent).metadata();
|
|
125
135
|
|
|
126
136
|
const contentType = res.contentType || pathToMimeType(iconUrl);
|
|
127
137
|
|
|
138
|
+
const content = await bufferToDataUrl(rawContent, contentType);
|
|
139
|
+
|
|
128
140
|
if (meta.width && meta.height) {
|
|
129
141
|
if (meta.width !== meta.height) {
|
|
130
142
|
processor.notSquare(meta.width, meta.height);
|
|
131
|
-
return null;
|
|
132
143
|
} else {
|
|
133
144
|
processor.square(meta.width);
|
|
134
145
|
|
|
@@ -142,10 +153,20 @@ export const checkIcon = async (
|
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
|
|
145
|
-
return
|
|
156
|
+
return {
|
|
157
|
+
content,
|
|
158
|
+
url: iconUrl,
|
|
159
|
+
width: meta.width || null,
|
|
160
|
+
height: meta.height || null
|
|
161
|
+
}
|
|
146
162
|
}
|
|
147
163
|
|
|
148
|
-
return
|
|
164
|
+
return {
|
|
165
|
+
content: null,
|
|
166
|
+
url: iconUrl,
|
|
167
|
+
width: null,
|
|
168
|
+
height: null
|
|
169
|
+
};
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
export const mergeUrlAndPath = (baseUrl: string, absoluteOrRelativePath: string): string => {
|
|
@@ -189,6 +210,13 @@ export const bufferToDataUrl = (buffer: Buffer, mimeType: string): string => {
|
|
|
189
210
|
return `data:${mimeType};base64,${buffer.toString('base64')}`;
|
|
190
211
|
}
|
|
191
212
|
|
|
213
|
+
export const filePathToDataUrl = async (filePath: string): Promise<string> => {
|
|
214
|
+
const readStream = await filePathToReadableStream(filePath);
|
|
215
|
+
const rawContent = await readableStreamToBuffer(readStream);
|
|
216
|
+
const contentType = pathToMimeType(filePath);
|
|
217
|
+
return bufferToDataUrl(rawContent, contentType);
|
|
218
|
+
}
|
|
219
|
+
|
|
192
220
|
export const fetchFetcher: Fetcher = async (url, contentType) => {
|
|
193
221
|
const res = await fetch(url, {
|
|
194
222
|
headers: {
|
package/src/touch-icon.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CheckerMessage, CheckerStatus, Fetcher, MessageId, TouchIconIconReport, TouchIconReport, TouchIconTitleReport } from "./types";
|
|
2
2
|
import { HTMLElement } from 'node-html-parser'
|
|
3
|
-
import { CheckIconProcessor, checkIcon, fetchFetcher, mergeUrlAndPath } from "./helper";
|
|
3
|
+
import { CheckIconOutput, CheckIconProcessor, checkIcon, fetchFetcher, mergeUrlAndPath } from "./helper";
|
|
4
4
|
|
|
5
5
|
export const checkTouchIconTitle = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<TouchIconTitleReport> => {
|
|
6
6
|
const messages: CheckerMessage[] = [];
|
|
@@ -59,7 +59,7 @@ export const checkTouchIconTitle = async (baseUrl: string, head: HTMLElement | n
|
|
|
59
59
|
|
|
60
60
|
export const checkTouchIconIcon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<TouchIconIconReport> => {
|
|
61
61
|
const messages: CheckerMessage[] = [];
|
|
62
|
-
let touchIcon:
|
|
62
|
+
let touchIcon: CheckIconOutput | null = null;
|
|
63
63
|
|
|
64
64
|
if (!head) {
|
|
65
65
|
messages.push({
|
|
@@ -176,7 +176,7 @@ export const checkTouchIconIcon = async (baseUrl: string, head: HTMLElement | nu
|
|
|
176
176
|
undefined
|
|
177
177
|
);
|
|
178
178
|
|
|
179
|
-
return { messages, touchIcon };
|
|
179
|
+
return { messages, touchIcon: touchIcon ? touchIcon.content : null };
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export const getDuplicatedSizes = (sizes: (string | undefined)[]): (string | undefined)[] => {
|
package/src/types.ts
CHANGED
|
@@ -79,7 +79,16 @@ export enum MessageId {
|
|
|
79
79
|
manifestIconNotSquare,
|
|
80
80
|
manifestIconRightSize,
|
|
81
81
|
manifestIconSquare,
|
|
82
|
-
manifestIconWrongSize
|
|
82
|
+
manifestIconWrongSize,
|
|
83
|
+
|
|
84
|
+
googleNoRobotsFile,
|
|
85
|
+
googleRobotsFileFound,
|
|
86
|
+
googleIcoBlockedByRobots,
|
|
87
|
+
googleIcoAllowedByRobots,
|
|
88
|
+
googleSvgIconBlockedByRobots,
|
|
89
|
+
googleSvgIconAllowedByRobots,
|
|
90
|
+
googlePngIconBlockedByRobots,
|
|
91
|
+
googlePngIconAllowedByRobots,
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
export type CheckerMessage = {
|
|
@@ -96,9 +105,26 @@ export type FetchResponse = {
|
|
|
96
105
|
|
|
97
106
|
export type Fetcher = (url: string, contentType?: string) => Promise<FetchResponse>;
|
|
98
107
|
|
|
108
|
+
export type CheckedIcon = {
|
|
109
|
+
content: string | null,
|
|
110
|
+
url: string | null,
|
|
111
|
+
width: number | null,
|
|
112
|
+
height: number | null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type DesktopSingleReport = {
|
|
116
|
+
messages: CheckerMessage[],
|
|
117
|
+
icon: CheckedIcon | null,
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
export type DesktopFaviconReport = {
|
|
100
121
|
messages: CheckerMessage[],
|
|
101
122
|
icon: string | null,
|
|
123
|
+
icons: {
|
|
124
|
+
png: CheckedIcon | null,
|
|
125
|
+
ico: CheckedIcon | null,
|
|
126
|
+
svg: CheckedIcon | null,
|
|
127
|
+
}
|
|
102
128
|
}
|
|
103
129
|
|
|
104
130
|
export type TouchIconTitleReport = {
|
|
@@ -127,3 +153,9 @@ export type FaviconReport = {
|
|
|
127
153
|
}
|
|
128
154
|
|
|
129
155
|
export type TouchIconReport = TouchIconIconReport & TouchIconTitleReport;
|
|
156
|
+
|
|
157
|
+
export type GoogleReport = {
|
|
158
|
+
messages: CheckerMessage[],
|
|
159
|
+
icon: string | null,
|
|
160
|
+
icons: CheckedIcon[]
|
|
161
|
+
}
|
package/src/web-app-manifest.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HTMLElement } from "node-html-parser";
|
|
2
2
|
import { CheckerMessage, CheckerStatus, Fetcher, MessageId, WebAppManifestReport } from "./types";
|
|
3
|
-
import { CheckIconProcessor, checkIcon, fetchFetcher, mergeUrlAndPath, pathToMimeType } from "./helper";
|
|
3
|
+
import { CheckIconOutput, CheckIconProcessor, checkIcon, fetchFetcher, mergeUrlAndPath, pathToMimeType } from "./helper";
|
|
4
4
|
|
|
5
5
|
export const checkWebAppManifest = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<WebAppManifestReport> => {
|
|
6
6
|
const messages: CheckerMessage[] = [];
|
|
@@ -101,7 +101,7 @@ const readableStreamToJson = async (stream: ReadableStream): Promise<any> => {
|
|
|
101
101
|
|
|
102
102
|
export const checkWebAppManifestFile = async (manifest: any, baseUrl: string, fetcher: Fetcher): Promise<WebAppManifestReport> => {
|
|
103
103
|
const messages: CheckerMessage[] = [];
|
|
104
|
-
let icon = null;
|
|
104
|
+
let icon: CheckIconOutput | null = null;
|
|
105
105
|
|
|
106
106
|
const name = manifest.name || undefined;
|
|
107
107
|
if (!name) {
|
|
@@ -257,5 +257,5 @@ export const checkWebAppManifestFile = async (manifest: any, baseUrl: string, fe
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
260
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon: icon ? icon.content : null };
|
|
261
261
|
}
|
package/dist/web-manifest.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { HTMLElement } from "node-html-parser";
|
|
2
|
-
import { Fetcher, WebManifestReport } from "./types";
|
|
3
|
-
export declare const checkWebAppManifest: (baseUrl: string, head: HTMLElement | null, fetcher?: Fetcher) => Promise<WebManifestReport>;
|
|
4
|
-
export declare const checkWebManifestFile: (manifest: any, baseUrl: string, fetcher: Fetcher) => Promise<WebManifestReport>;
|