@realfavicongenerator/check-favicon 0.4.6 → 0.4.8

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.
@@ -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<CheckerMessage[]> => {
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,13 +15,19 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
15
15
  text: 'No <head> element'
16
16
  });
17
17
 
18
- return messages;
18
+ return {
19
+ messages,
20
+ icon : { content: null, url: null, width: null, height: null }
21
+ };
19
22
  }
20
23
 
21
24
  const icos =
22
25
  head.querySelectorAll('link[rel="shortcut icon"]') ||
23
26
  head.querySelectorAll('link[rel="icon"][type="image/x-icon"]');
24
27
 
28
+ let iconUrl: string | null = null;
29
+ let images;
30
+
25
31
  if (icos.length === 0) {
26
32
  messages.push({
27
33
  status: CheckerStatus.Error,
@@ -49,7 +55,7 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
49
55
  text: 'The ICO markup has no href attribute'
50
56
  });
51
57
  } else {
52
- const iconUrl = mergeUrlAndPath(url, href);
58
+ iconUrl = mergeUrlAndPath(url, href);
53
59
  const iconResponse = await fetcher(iconUrl, 'image/x-icon');
54
60
  if (iconResponse.status === 404) {
55
61
  messages.push({
@@ -71,7 +77,7 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
71
77
  });
72
78
 
73
79
  const iconBuffer = await readableStreamToBuffer(iconResponse.readableStream);
74
- const images = await decodeIco(iconBuffer);
80
+ images = await decodeIco(iconBuffer);
75
81
 
76
82
  const imageSizes = images.map(image => `${image.width}x${image.height}`);
77
83
 
@@ -106,5 +112,23 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
106
112
  }
107
113
  }
108
114
 
109
- return messages;
115
+ let content: string | null = null;
116
+ const theIcon: CheckedIcon = {
117
+ content: null,
118
+ url: iconUrl,
119
+ width: null,
120
+ height: null
121
+ };
122
+ if (images) {
123
+ const image = images[0];
124
+ const mimeType = (image.type === "bmp") ? "image/bmp" : "image/png";
125
+ theIcon.content = await bufferToDataUrl(Buffer.from(image.data), mimeType);
126
+ theIcon.width = image.width;
127
+ theIcon.height = image.height;
128
+ }
129
+
130
+ return {
131
+ messages,
132
+ icon: theIcon,
133
+ };
110
134
  }
@@ -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
+ }
@@ -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')).toBeNull();
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')).toBeNull();
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)).toBeNull();
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'
@@ -117,6 +132,11 @@ test('mergeUrlAndPath', () => {
117
132
  expect(mergeUrlAndPath('https://example.com/sub-page', 'some/path')).toBe('https://example.com/sub-page/some/path');
118
133
 
119
134
  expect(mergeUrlAndPath('https://example.com', 'https://elsewhere.com/some-path')).toBe('https://elsewhere.com/some-path');
135
+
136
+ // Protocol-relative URL
137
+ // For https://github.com/RealFaviconGenerator/core/issues/2
138
+ expect(mergeUrlAndPath('https://example.com', '//elsewhere.com/some-path')).toBe('https://elsewhere.com/some-path');
139
+ expect(mergeUrlAndPath('http://example.com', '//elsewhere.com/some-other/path')).toBe('http://elsewhere.com/some-other/path');
120
140
  })
121
141
 
122
142
  test('parseSizesAttribute', () => {
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(str);
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<string | null> => {
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 content = await readableStreamToBuffer(res.readableStream);
124
- const meta = await sharp(content).metadata();
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 bufferToDataUrl(content, contentType);
156
+ return {
157
+ content,
158
+ url: iconUrl,
159
+ width: meta.width || null,
160
+ height: meta.height || null
161
+ }
146
162
  }
147
163
 
148
- return null;
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 => {
@@ -156,8 +177,11 @@ export const mergeUrlAndPath = (baseUrl: string, absoluteOrRelativePath: string)
156
177
 
157
178
  const url = new URL(baseUrl);
158
179
 
159
- // If the path starts with a slash, replace the pathname
160
- if (absoluteOrRelativePath.startsWith('/')) {
180
+ // Protocol-relative URL
181
+ if (absoluteOrRelativePath.startsWith('//')) {
182
+ return `${url.protocol}${absoluteOrRelativePath}`;
183
+ } else if (absoluteOrRelativePath.startsWith('/')) {
184
+ // If the path starts with a slash, replace the pathname
161
185
  return `${url.origin}${absoluteOrRelativePath}`;
162
186
  } else {
163
187
  // Otherwise, append the path to the existing pathname
@@ -186,6 +210,13 @@ export const bufferToDataUrl = (buffer: Buffer, mimeType: string): string => {
186
210
  return `data:${mimeType};base64,${buffer.toString('base64')}`;
187
211
  }
188
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
+
189
220
  export const fetchFetcher: Fetcher = async (url, contentType) => {
190
221
  const res = await fetch(url, {
191
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: string | null = null;
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
+ }
@@ -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
  }
@@ -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>;