@realfavicongenerator/check-favicon 0.5.0 → 0.5.2

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.
@@ -36,21 +36,16 @@ const checkIcoFavicon = (url, head, fetcher) => __awaiter(void 0, void 0, void 0
36
36
  ];
37
37
  let iconUrl = null;
38
38
  let images;
39
- if (icos.length === 0) {
40
- messages.push({
41
- status: types_1.CheckerStatus.Error,
42
- id: types_1.MessageId.noIcoFavicon,
43
- text: 'There is no ICO favicon'
44
- });
45
- }
46
- else if (icos.length > 1) {
39
+ let isDeclared = false;
40
+ if (icos.length > 1) {
47
41
  messages.push({
48
42
  status: types_1.CheckerStatus.Error,
49
43
  id: types_1.MessageId.multipleIcoFavicons,
50
44
  text: `There are ${icos.length} ICO favicons`
51
45
  });
52
46
  }
53
- else {
47
+ else if (icos.length === 1) {
48
+ isDeclared = true;
54
49
  messages.push({
55
50
  status: types_1.CheckerStatus.Ok,
56
51
  id: types_1.MessageId.icoFaviconDeclared,
@@ -66,15 +61,35 @@ const checkIcoFavicon = (url, head, fetcher) => __awaiter(void 0, void 0, void 0
66
61
  }
67
62
  else {
68
63
  iconUrl = (0, helper_1.mergeUrlAndPath)(url, href);
69
- const iconResponse = yield fetcher(iconUrl, 'image/x-icon');
70
- if (iconResponse.status === 404) {
64
+ }
65
+ }
66
+ else {
67
+ // No declared ICO favicon, try the implicit /favicon.ico convention
68
+ iconUrl = (0, helper_1.mergeUrlAndPath)(url, '/favicon.ico');
69
+ }
70
+ // If we have an iconUrl (either from declaration or implicit), try to fetch it
71
+ if (iconUrl) {
72
+ const iconResponse = yield fetcher(iconUrl, 'image/x-icon');
73
+ if (iconResponse.status === 404) {
74
+ if (isDeclared) {
71
75
  messages.push({
72
76
  status: types_1.CheckerStatus.Error,
73
77
  id: types_1.MessageId.icoFavicon404,
74
78
  text: `ICO favicon not found at ${iconUrl}`
75
79
  });
76
80
  }
77
- else if (iconResponse.status >= 300 || !iconResponse.readableStream) {
81
+ else {
82
+ // Implicit favicon not found, report no ICO favicon
83
+ messages.push({
84
+ status: types_1.CheckerStatus.Error,
85
+ id: types_1.MessageId.noIcoFavicon,
86
+ text: 'There is no ICO favicon'
87
+ });
88
+ iconUrl = null;
89
+ }
90
+ }
91
+ else if (iconResponse.status >= 300 || !iconResponse.readableStream) {
92
+ if (isDeclared) {
78
93
  messages.push({
79
94
  status: types_1.CheckerStatus.Error,
80
95
  id: types_1.MessageId.icoFaviconCannotGet,
@@ -82,42 +97,57 @@ const checkIcoFavicon = (url, head, fetcher) => __awaiter(void 0, void 0, void 0
82
97
  });
83
98
  }
84
99
  else {
100
+ // Implicit favicon cannot be fetched, report no ICO favicon
101
+ messages.push({
102
+ status: types_1.CheckerStatus.Error,
103
+ id: types_1.MessageId.noIcoFavicon,
104
+ text: 'There is no ICO favicon'
105
+ });
106
+ iconUrl = null;
107
+ }
108
+ }
109
+ else {
110
+ if (!isDeclared) {
111
+ messages.push({
112
+ status: types_1.CheckerStatus.Ok,
113
+ id: types_1.MessageId.icoFaviconImplicitInRoot,
114
+ text: 'An implicit ICO favicon is found at /favicon.ico'
115
+ });
116
+ }
117
+ messages.push({
118
+ status: types_1.CheckerStatus.Ok,
119
+ id: types_1.MessageId.icoFaviconDownloadable,
120
+ text: 'ICO favicon found'
121
+ });
122
+ const iconBuffer = yield (0, helper_1.readableStreamToBuffer)(iconResponse.readableStream);
123
+ images = (0, decode_ico_1.default)(iconBuffer);
124
+ const imageSizes = images.map(image => `${image.width}x${image.height}`);
125
+ const expectedSizes = exports.IcoFaviconSizes.map(size => `${size}x${size}`);
126
+ const extraSizes = imageSizes.filter(size => !expectedSizes.includes(size));
127
+ if (extraSizes.length > 0) {
128
+ messages.push({
129
+ status: types_1.CheckerStatus.Warning,
130
+ id: types_1.MessageId.icoFaviconExtraSizes,
131
+ text: `Extra sizes found in ICO favicon: ${extraSizes.join(', ')}`
132
+ });
133
+ }
134
+ const missingSizes = expectedSizes.filter(size => !imageSizes.includes(size));
135
+ if (missingSizes.length > 0) {
136
+ messages.push({
137
+ status: types_1.CheckerStatus.Warning,
138
+ id: types_1.MessageId.icoFaviconMissingSizes,
139
+ text: `Missing sizes in ICO favicon: ${missingSizes.join(', ')}`
140
+ });
141
+ }
142
+ if (extraSizes.length === 0 && missingSizes.length === 0) {
85
143
  messages.push({
86
144
  status: types_1.CheckerStatus.Ok,
87
- id: types_1.MessageId.icoFaviconDownloadable,
88
- text: 'ICO favicon found'
145
+ id: types_1.MessageId.icoFaviconExpectedSizes,
146
+ text: `The ICO favicon has the expected sizes (${imageSizes.join(', ')})`
89
147
  });
90
- const iconBuffer = yield (0, helper_1.readableStreamToBuffer)(iconResponse.readableStream);
91
- images = yield (0, decode_ico_1.default)(iconBuffer);
92
- const imageSizes = images.map(image => `${image.width}x${image.height}`);
93
- const expectedSizes = exports.IcoFaviconSizes.map(size => `${size}x${size}`);
94
- const extraSizes = imageSizes.filter(size => !expectedSizes.includes(size));
95
- if (extraSizes.length > 0) {
96
- messages.push({
97
- status: types_1.CheckerStatus.Warning,
98
- id: types_1.MessageId.icoFaviconExtraSizes,
99
- text: `Extra sizes found in ICO favicon: ${extraSizes.join(', ')}`
100
- });
101
- }
102
- const missingSizes = expectedSizes.filter(size => !imageSizes.includes(size));
103
- if (missingSizes.length > 0) {
104
- messages.push({
105
- status: types_1.CheckerStatus.Warning,
106
- id: types_1.MessageId.icoFaviconMissingSizes,
107
- text: `Missing sizes in ICO favicon: ${missingSizes.join(', ')}`
108
- });
109
- }
110
- if (extraSizes.length === 0 && missingSizes.length === 0) {
111
- messages.push({
112
- status: types_1.CheckerStatus.Ok,
113
- id: types_1.MessageId.icoFaviconExpectedSizes,
114
- text: `The ICO favicon has the expected sizes (${imageSizes.join(', ')})`
115
- });
116
- }
117
148
  }
118
149
  }
119
150
  }
120
- let content = null;
121
151
  const theIcon = {
122
152
  content: null,
123
153
  url: iconUrl,
@@ -127,7 +157,7 @@ const checkIcoFavicon = (url, head, fetcher) => __awaiter(void 0, void 0, void 0
127
157
  if (images) {
128
158
  const image = images[0];
129
159
  const mimeType = (image.type === "bmp") ? "image/bmp" : "image/png";
130
- theIcon.content = yield (0, helper_1.bufferToDataUrl)(Buffer.from(image.data), mimeType);
160
+ theIcon.content = (0, helper_1.bufferToDataUrl)(Buffer.from(image.data.buffer, image.data.byteOffset, image.data.byteLength), mimeType);
131
161
  theIcon.width = image.width;
132
162
  theIcon.height = image.height;
133
163
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const node_html_parser_1 = require("node-html-parser");
13
+ const ico_1 = require("./ico");
14
+ const types_1 = require("../types");
15
+ const helper_1 = require("../helper");
16
+ const test_helper_1 = require("../test-helper");
17
+ const runIcoTest = (headFragment_1, output_1, ...args_1) => __awaiter(void 0, [headFragment_1, output_1, ...args_1], void 0, function* (headFragment, output, fetchDatabase = {}, checkContent = true) {
18
+ const root = headFragment ? (0, node_html_parser_1.parse)(headFragment) : null;
19
+ const result = yield (0, ico_1.checkIcoFavicon)('https://example.com/', root, (0, test_helper_1.testFetcher)(fetchDatabase));
20
+ const filteredMessages = result.messages.map(m => ({ status: m.status, id: m.id }));
21
+ expect(filteredMessages).toEqual(output.messages);
22
+ // Check icon properties - icon is always returned by checkIcoFavicon
23
+ const resultIcon = result.icon;
24
+ const outputIcon = output.icon;
25
+ expect(resultIcon.url).toEqual(outputIcon.url);
26
+ expect(resultIcon.width).toEqual(outputIcon.width);
27
+ expect(resultIcon.height).toEqual(outputIcon.height);
28
+ // For content, just check if it's null or not null unless exact match is needed
29
+ if (checkContent && outputIcon.content === null) {
30
+ expect(resultIcon.content).toBeNull();
31
+ }
32
+ else if (checkContent && outputIcon.content !== null) {
33
+ expect(resultIcon.content).not.toBeNull();
34
+ expect(resultIcon.content).toMatch(/^data:image\/(png|bmp);base64,/);
35
+ }
36
+ });
37
+ test('checkIcoFavicon - noHead', () => __awaiter(void 0, void 0, void 0, function* () {
38
+ yield runIcoTest(null, {
39
+ messages: [{
40
+ status: types_1.CheckerStatus.Error,
41
+ id: types_1.MessageId.noHead,
42
+ }],
43
+ icon: {
44
+ content: null,
45
+ url: null,
46
+ width: null,
47
+ height: null,
48
+ }
49
+ });
50
+ }));
51
+ test('checkIcoFavicon - noIcoFavicon', () => __awaiter(void 0, void 0, void 0, function* () {
52
+ yield runIcoTest(`<title>Some text</title>`, {
53
+ messages: [{
54
+ status: types_1.CheckerStatus.Error,
55
+ id: types_1.MessageId.noIcoFavicon,
56
+ }],
57
+ icon: {
58
+ content: null,
59
+ url: null,
60
+ width: null,
61
+ height: null,
62
+ }
63
+ });
64
+ }));
65
+ test('checkIcoFavicon - implicit /favicon.ico when not declared', () => __awaiter(void 0, void 0, void 0, function* () {
66
+ const testIconPath = './fixtures/simple-ico.ico';
67
+ yield runIcoTest(`<title>Some text</title>`, {
68
+ messages: [{
69
+ status: types_1.CheckerStatus.Ok,
70
+ id: types_1.MessageId.icoFaviconImplicitInRoot,
71
+ }, {
72
+ status: types_1.CheckerStatus.Ok,
73
+ id: types_1.MessageId.icoFaviconDownloadable,
74
+ }, {
75
+ status: types_1.CheckerStatus.Ok,
76
+ id: types_1.MessageId.icoFaviconExpectedSizes,
77
+ }],
78
+ icon: {
79
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
80
+ url: 'https://example.com/favicon.ico',
81
+ width: 48,
82
+ height: 48,
83
+ }
84
+ }, {
85
+ 'https://example.com/favicon.ico': {
86
+ status: 200,
87
+ contentType: 'image/x-icon',
88
+ readableStream: yield (0, helper_1.filePathToReadableStream)(testIconPath)
89
+ }
90
+ });
91
+ }));
92
+ test('checkIcoFavicon - multipleIcoFavicons with shortcut icon', () => __awaiter(void 0, void 0, void 0, function* () {
93
+ yield runIcoTest(`
94
+ <link rel="shortcut icon" href="/favicon1.ico" />
95
+ <link rel="shortcut icon" href="/favicon2.ico" />
96
+ `, {
97
+ messages: [{
98
+ status: types_1.CheckerStatus.Error,
99
+ id: types_1.MessageId.multipleIcoFavicons,
100
+ }],
101
+ icon: {
102
+ content: null,
103
+ url: null,
104
+ width: null,
105
+ height: null,
106
+ }
107
+ });
108
+ }));
109
+ test('checkIcoFavicon - multipleIcoFavicons with type="image/x-icon"', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ yield runIcoTest(`
111
+ <link rel="icon" type="image/x-icon" href="/favicon1.ico" />
112
+ <link rel="icon" type="image/x-icon" href="/favicon2.ico" />
113
+ `, {
114
+ messages: [{
115
+ status: types_1.CheckerStatus.Error,
116
+ id: types_1.MessageId.multipleIcoFavicons,
117
+ }],
118
+ icon: {
119
+ content: null,
120
+ url: null,
121
+ width: null,
122
+ height: null,
123
+ }
124
+ });
125
+ }));
126
+ test('checkIcoFavicon - icoFaviconDeclared & noIcoFaviconHref', () => __awaiter(void 0, void 0, void 0, function* () {
127
+ yield runIcoTest(`<link rel="shortcut icon" />`, {
128
+ messages: [{
129
+ status: types_1.CheckerStatus.Ok,
130
+ id: types_1.MessageId.icoFaviconDeclared,
131
+ }, {
132
+ status: types_1.CheckerStatus.Error,
133
+ id: types_1.MessageId.noIcoFaviconHref,
134
+ }],
135
+ icon: {
136
+ content: null,
137
+ url: null,
138
+ width: null,
139
+ height: null,
140
+ }
141
+ });
142
+ }));
143
+ test('checkIcoFavicon - icoFaviconDeclared & icoFavicon404', () => __awaiter(void 0, void 0, void 0, function* () {
144
+ yield runIcoTest(`<link rel="shortcut icon" href="/favicon.ico" />`, {
145
+ messages: [{
146
+ status: types_1.CheckerStatus.Ok,
147
+ id: types_1.MessageId.icoFaviconDeclared,
148
+ }, {
149
+ status: types_1.CheckerStatus.Error,
150
+ id: types_1.MessageId.icoFavicon404,
151
+ }],
152
+ icon: {
153
+ content: null,
154
+ url: 'https://example.com/favicon.ico',
155
+ width: null,
156
+ height: null,
157
+ }
158
+ });
159
+ }));
160
+ test('checkIcoFavicon - icoFaviconDeclared & icoFaviconCannotGet', () => __awaiter(void 0, void 0, void 0, function* () {
161
+ yield runIcoTest(`<link rel="shortcut icon" href="/favicon.ico" />`, {
162
+ messages: [{
163
+ status: types_1.CheckerStatus.Ok,
164
+ id: types_1.MessageId.icoFaviconDeclared,
165
+ }, {
166
+ status: types_1.CheckerStatus.Error,
167
+ id: types_1.MessageId.icoFaviconCannotGet,
168
+ }],
169
+ icon: {
170
+ content: null,
171
+ url: 'https://example.com/favicon.ico',
172
+ width: null,
173
+ height: null,
174
+ }
175
+ }, {
176
+ 'https://example.com/favicon.ico': {
177
+ status: 403,
178
+ contentType: 'image/x-icon'
179
+ }
180
+ });
181
+ }));
182
+ test('checkIcoFavicon - icoFaviconDeclared & icoFaviconDownloadable & icoFaviconExpectedSizes', () => __awaiter(void 0, void 0, void 0, function* () {
183
+ const testIconPath = './fixtures/simple-ico.ico';
184
+ yield runIcoTest(`<link rel="shortcut icon" href="/favicon.ico" />`, {
185
+ messages: [{
186
+ status: types_1.CheckerStatus.Ok,
187
+ id: types_1.MessageId.icoFaviconDeclared,
188
+ }, {
189
+ status: types_1.CheckerStatus.Ok,
190
+ id: types_1.MessageId.icoFaviconDownloadable,
191
+ }, {
192
+ status: types_1.CheckerStatus.Ok,
193
+ id: types_1.MessageId.icoFaviconExpectedSizes,
194
+ }],
195
+ icon: {
196
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
197
+ url: 'https://example.com/favicon.ico',
198
+ width: 48,
199
+ height: 48,
200
+ }
201
+ }, {
202
+ 'https://example.com/favicon.ico': {
203
+ status: 200,
204
+ contentType: 'image/x-icon',
205
+ readableStream: yield (0, helper_1.filePathToReadableStream)(testIconPath)
206
+ }
207
+ });
208
+ }));
209
+ test('checkIcoFavicon - using type="image/x-icon"', () => __awaiter(void 0, void 0, void 0, function* () {
210
+ const testIconPath = './fixtures/simple-ico.ico';
211
+ yield runIcoTest(`<link rel="icon" type="image/x-icon" href="/favicon.ico" />`, {
212
+ messages: [{
213
+ status: types_1.CheckerStatus.Ok,
214
+ id: types_1.MessageId.icoFaviconDeclared,
215
+ }, {
216
+ status: types_1.CheckerStatus.Ok,
217
+ id: types_1.MessageId.icoFaviconDownloadable,
218
+ }, {
219
+ status: types_1.CheckerStatus.Ok,
220
+ id: types_1.MessageId.icoFaviconExpectedSizes,
221
+ }],
222
+ icon: {
223
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
224
+ url: 'https://example.com/favicon.ico',
225
+ width: 48,
226
+ height: 48,
227
+ }
228
+ }, {
229
+ 'https://example.com/favicon.ico': {
230
+ status: 200,
231
+ contentType: 'image/x-icon',
232
+ readableStream: yield (0, helper_1.filePathToReadableStream)(testIconPath)
233
+ }
234
+ });
235
+ }));
236
+ // For https://github.com/RealFaviconGenerator/core/issues/2
237
+ test('checkIcoFavicon - Protocol-relative URL', () => __awaiter(void 0, void 0, void 0, function* () {
238
+ const testIconPath = './fixtures/simple-ico.ico';
239
+ yield runIcoTest(`<link rel="shortcut icon" href="//example.com/favicon.ico" />`, {
240
+ messages: [{
241
+ status: types_1.CheckerStatus.Ok,
242
+ id: types_1.MessageId.icoFaviconDeclared,
243
+ }, {
244
+ status: types_1.CheckerStatus.Ok,
245
+ id: types_1.MessageId.icoFaviconDownloadable,
246
+ }, {
247
+ status: types_1.CheckerStatus.Ok,
248
+ id: types_1.MessageId.icoFaviconExpectedSizes,
249
+ }],
250
+ icon: {
251
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
252
+ url: 'https://example.com/favicon.ico',
253
+ width: 48,
254
+ height: 48,
255
+ }
256
+ }, {
257
+ 'https://example.com/favicon.ico': {
258
+ status: 200,
259
+ contentType: 'image/x-icon',
260
+ readableStream: yield (0, helper_1.filePathToReadableStream)(testIconPath)
261
+ }
262
+ });
263
+ }));
package/dist/types.d.ts CHANGED
@@ -17,70 +17,71 @@ export declare enum MessageId {
17
17
  noIcoFavicon = 10,
18
18
  multipleIcoFavicons = 11,
19
19
  icoFaviconDeclared = 12,
20
- noIcoFaviconHref = 13,
21
- icoFavicon404 = 14,
22
- icoFaviconCannotGet = 15,
23
- icoFaviconDownloadable = 16,
24
- icoFaviconExtraSizes = 17,
25
- icoFaviconMissingSizes = 18,
26
- icoFaviconExpectedSizes = 19,
27
- noDesktopPngFavicon = 20,
28
- no96x96DesktopPngFavicon = 21,
29
- desktopPngFaviconDeclared = 22,
30
- noDesktopPngFaviconHref = 23,
31
- desktopPngFaviconCannotGet = 24,
32
- desktopPngFaviconDownloadable = 25,
33
- desktopPngFavicon404 = 26,
34
- desktopPngFaviconWrongSize = 27,
35
- desktopPngFaviconRightSize = 28,
36
- noTouchWebAppTitle = 29,
37
- multipleTouchWebAppTitles = 30,
38
- emptyTouchWebAppTitle = 31,
39
- touchWebAppTitleDeclared = 32,
40
- noTouchIcon = 33,
41
- duplicatedTouchIconSizes = 34,
42
- touchIconWithSize = 35,
43
- touchIconDeclared = 36,
44
- noTouchIconHref = 37,
45
- touchIcon404 = 38,
46
- touchIconCannotGet = 39,
47
- touchIconDownloadable = 40,
48
- touchIconSquare = 41,
49
- touchIcon180x180 = 42,
50
- touchIconNotSquare = 43,
51
- touchIconWrongSize = 44,
52
- noManifest = 45,
53
- noManifestHref = 46,
54
- manifest404 = 47,
55
- manifestCannotGet = 48,
56
- manifestInvalidJson = 49,
57
- manifestName = 50,
58
- noManifestName = 51,
59
- manifestShortName = 52,
60
- noManifestShortName = 53,
61
- manifestBackgroundColor = 54,
62
- noManifestBackgroundColor = 55,
63
- manifestThemeColor = 56,
64
- noManifestThemeColor = 57,
65
- noManifestIcons = 58,
66
- noManifestIcon = 59,
67
- manifestIconDeclared = 60,
68
- manifestIconCannotGet = 61,
69
- manifestIconDownloadable = 62,
70
- manifestIcon404 = 63,
71
- manifestIconNoHref = 64,
72
- manifestIconNotSquare = 65,
73
- manifestIconRightSize = 66,
74
- manifestIconSquare = 67,
75
- manifestIconWrongSize = 68,
76
- googleNoRobotsFile = 69,
77
- googleRobotsFileFound = 70,
78
- googleIcoBlockedByRobots = 71,
79
- googleIcoAllowedByRobots = 72,
80
- googleSvgIconBlockedByRobots = 73,
81
- googleSvgIconAllowedByRobots = 74,
82
- googlePngIconBlockedByRobots = 75,
83
- googlePngIconAllowedByRobots = 76
20
+ icoFaviconImplicitInRoot = 13,
21
+ noIcoFaviconHref = 14,
22
+ icoFavicon404 = 15,
23
+ icoFaviconCannotGet = 16,
24
+ icoFaviconDownloadable = 17,
25
+ icoFaviconExtraSizes = 18,
26
+ icoFaviconMissingSizes = 19,
27
+ icoFaviconExpectedSizes = 20,
28
+ noDesktopPngFavicon = 21,
29
+ no96x96DesktopPngFavicon = 22,
30
+ desktopPngFaviconDeclared = 23,
31
+ noDesktopPngFaviconHref = 24,
32
+ desktopPngFaviconCannotGet = 25,
33
+ desktopPngFaviconDownloadable = 26,
34
+ desktopPngFavicon404 = 27,
35
+ desktopPngFaviconWrongSize = 28,
36
+ desktopPngFaviconRightSize = 29,
37
+ noTouchWebAppTitle = 30,
38
+ multipleTouchWebAppTitles = 31,
39
+ emptyTouchWebAppTitle = 32,
40
+ touchWebAppTitleDeclared = 33,
41
+ noTouchIcon = 34,
42
+ duplicatedTouchIconSizes = 35,
43
+ touchIconWithSize = 36,
44
+ touchIconDeclared = 37,
45
+ noTouchIconHref = 38,
46
+ touchIcon404 = 39,
47
+ touchIconCannotGet = 40,
48
+ touchIconDownloadable = 41,
49
+ touchIconSquare = 42,
50
+ touchIcon180x180 = 43,
51
+ touchIconNotSquare = 44,
52
+ touchIconWrongSize = 45,
53
+ noManifest = 46,
54
+ noManifestHref = 47,
55
+ manifest404 = 48,
56
+ manifestCannotGet = 49,
57
+ manifestInvalidJson = 50,
58
+ manifestName = 51,
59
+ noManifestName = 52,
60
+ manifestShortName = 53,
61
+ noManifestShortName = 54,
62
+ manifestBackgroundColor = 55,
63
+ noManifestBackgroundColor = 56,
64
+ manifestThemeColor = 57,
65
+ noManifestThemeColor = 58,
66
+ noManifestIcons = 59,
67
+ noManifestIcon = 60,
68
+ manifestIconDeclared = 61,
69
+ manifestIconCannotGet = 62,
70
+ manifestIconDownloadable = 63,
71
+ manifestIcon404 = 64,
72
+ manifestIconNoHref = 65,
73
+ manifestIconNotSquare = 66,
74
+ manifestIconRightSize = 67,
75
+ manifestIconSquare = 68,
76
+ manifestIconWrongSize = 69,
77
+ googleNoRobotsFile = 70,
78
+ googleRobotsFileFound = 71,
79
+ googleIcoBlockedByRobots = 72,
80
+ googleIcoAllowedByRobots = 73,
81
+ googleSvgIconBlockedByRobots = 74,
82
+ googleSvgIconAllowedByRobots = 75,
83
+ googlePngIconBlockedByRobots = 76,
84
+ googlePngIconAllowedByRobots = 77
84
85
  }
85
86
  export type CheckerMessage = {
86
87
  status: CheckerStatus;
package/dist/types.js CHANGED
@@ -22,68 +22,69 @@ var MessageId;
22
22
  MessageId[MessageId["noIcoFavicon"] = 10] = "noIcoFavicon";
23
23
  MessageId[MessageId["multipleIcoFavicons"] = 11] = "multipleIcoFavicons";
24
24
  MessageId[MessageId["icoFaviconDeclared"] = 12] = "icoFaviconDeclared";
25
- MessageId[MessageId["noIcoFaviconHref"] = 13] = "noIcoFaviconHref";
26
- MessageId[MessageId["icoFavicon404"] = 14] = "icoFavicon404";
27
- MessageId[MessageId["icoFaviconCannotGet"] = 15] = "icoFaviconCannotGet";
28
- MessageId[MessageId["icoFaviconDownloadable"] = 16] = "icoFaviconDownloadable";
29
- MessageId[MessageId["icoFaviconExtraSizes"] = 17] = "icoFaviconExtraSizes";
30
- MessageId[MessageId["icoFaviconMissingSizes"] = 18] = "icoFaviconMissingSizes";
31
- MessageId[MessageId["icoFaviconExpectedSizes"] = 19] = "icoFaviconExpectedSizes";
32
- MessageId[MessageId["noDesktopPngFavicon"] = 20] = "noDesktopPngFavicon";
33
- MessageId[MessageId["no96x96DesktopPngFavicon"] = 21] = "no96x96DesktopPngFavicon";
34
- MessageId[MessageId["desktopPngFaviconDeclared"] = 22] = "desktopPngFaviconDeclared";
35
- MessageId[MessageId["noDesktopPngFaviconHref"] = 23] = "noDesktopPngFaviconHref";
36
- MessageId[MessageId["desktopPngFaviconCannotGet"] = 24] = "desktopPngFaviconCannotGet";
37
- MessageId[MessageId["desktopPngFaviconDownloadable"] = 25] = "desktopPngFaviconDownloadable";
38
- MessageId[MessageId["desktopPngFavicon404"] = 26] = "desktopPngFavicon404";
39
- MessageId[MessageId["desktopPngFaviconWrongSize"] = 27] = "desktopPngFaviconWrongSize";
40
- MessageId[MessageId["desktopPngFaviconRightSize"] = 28] = "desktopPngFaviconRightSize";
41
- MessageId[MessageId["noTouchWebAppTitle"] = 29] = "noTouchWebAppTitle";
42
- MessageId[MessageId["multipleTouchWebAppTitles"] = 30] = "multipleTouchWebAppTitles";
43
- MessageId[MessageId["emptyTouchWebAppTitle"] = 31] = "emptyTouchWebAppTitle";
44
- MessageId[MessageId["touchWebAppTitleDeclared"] = 32] = "touchWebAppTitleDeclared";
45
- MessageId[MessageId["noTouchIcon"] = 33] = "noTouchIcon";
46
- MessageId[MessageId["duplicatedTouchIconSizes"] = 34] = "duplicatedTouchIconSizes";
47
- MessageId[MessageId["touchIconWithSize"] = 35] = "touchIconWithSize";
48
- MessageId[MessageId["touchIconDeclared"] = 36] = "touchIconDeclared";
49
- MessageId[MessageId["noTouchIconHref"] = 37] = "noTouchIconHref";
50
- MessageId[MessageId["touchIcon404"] = 38] = "touchIcon404";
51
- MessageId[MessageId["touchIconCannotGet"] = 39] = "touchIconCannotGet";
52
- MessageId[MessageId["touchIconDownloadable"] = 40] = "touchIconDownloadable";
53
- MessageId[MessageId["touchIconSquare"] = 41] = "touchIconSquare";
54
- MessageId[MessageId["touchIcon180x180"] = 42] = "touchIcon180x180";
55
- MessageId[MessageId["touchIconNotSquare"] = 43] = "touchIconNotSquare";
56
- MessageId[MessageId["touchIconWrongSize"] = 44] = "touchIconWrongSize";
57
- MessageId[MessageId["noManifest"] = 45] = "noManifest";
58
- MessageId[MessageId["noManifestHref"] = 46] = "noManifestHref";
59
- MessageId[MessageId["manifest404"] = 47] = "manifest404";
60
- MessageId[MessageId["manifestCannotGet"] = 48] = "manifestCannotGet";
61
- MessageId[MessageId["manifestInvalidJson"] = 49] = "manifestInvalidJson";
62
- MessageId[MessageId["manifestName"] = 50] = "manifestName";
63
- MessageId[MessageId["noManifestName"] = 51] = "noManifestName";
64
- MessageId[MessageId["manifestShortName"] = 52] = "manifestShortName";
65
- MessageId[MessageId["noManifestShortName"] = 53] = "noManifestShortName";
66
- MessageId[MessageId["manifestBackgroundColor"] = 54] = "manifestBackgroundColor";
67
- MessageId[MessageId["noManifestBackgroundColor"] = 55] = "noManifestBackgroundColor";
68
- MessageId[MessageId["manifestThemeColor"] = 56] = "manifestThemeColor";
69
- MessageId[MessageId["noManifestThemeColor"] = 57] = "noManifestThemeColor";
70
- MessageId[MessageId["noManifestIcons"] = 58] = "noManifestIcons";
71
- MessageId[MessageId["noManifestIcon"] = 59] = "noManifestIcon";
72
- MessageId[MessageId["manifestIconDeclared"] = 60] = "manifestIconDeclared";
73
- MessageId[MessageId["manifestIconCannotGet"] = 61] = "manifestIconCannotGet";
74
- MessageId[MessageId["manifestIconDownloadable"] = 62] = "manifestIconDownloadable";
75
- MessageId[MessageId["manifestIcon404"] = 63] = "manifestIcon404";
76
- MessageId[MessageId["manifestIconNoHref"] = 64] = "manifestIconNoHref";
77
- MessageId[MessageId["manifestIconNotSquare"] = 65] = "manifestIconNotSquare";
78
- MessageId[MessageId["manifestIconRightSize"] = 66] = "manifestIconRightSize";
79
- MessageId[MessageId["manifestIconSquare"] = 67] = "manifestIconSquare";
80
- MessageId[MessageId["manifestIconWrongSize"] = 68] = "manifestIconWrongSize";
81
- MessageId[MessageId["googleNoRobotsFile"] = 69] = "googleNoRobotsFile";
82
- MessageId[MessageId["googleRobotsFileFound"] = 70] = "googleRobotsFileFound";
83
- MessageId[MessageId["googleIcoBlockedByRobots"] = 71] = "googleIcoBlockedByRobots";
84
- MessageId[MessageId["googleIcoAllowedByRobots"] = 72] = "googleIcoAllowedByRobots";
85
- MessageId[MessageId["googleSvgIconBlockedByRobots"] = 73] = "googleSvgIconBlockedByRobots";
86
- MessageId[MessageId["googleSvgIconAllowedByRobots"] = 74] = "googleSvgIconAllowedByRobots";
87
- MessageId[MessageId["googlePngIconBlockedByRobots"] = 75] = "googlePngIconBlockedByRobots";
88
- MessageId[MessageId["googlePngIconAllowedByRobots"] = 76] = "googlePngIconAllowedByRobots";
25
+ MessageId[MessageId["icoFaviconImplicitInRoot"] = 13] = "icoFaviconImplicitInRoot";
26
+ MessageId[MessageId["noIcoFaviconHref"] = 14] = "noIcoFaviconHref";
27
+ MessageId[MessageId["icoFavicon404"] = 15] = "icoFavicon404";
28
+ MessageId[MessageId["icoFaviconCannotGet"] = 16] = "icoFaviconCannotGet";
29
+ MessageId[MessageId["icoFaviconDownloadable"] = 17] = "icoFaviconDownloadable";
30
+ MessageId[MessageId["icoFaviconExtraSizes"] = 18] = "icoFaviconExtraSizes";
31
+ MessageId[MessageId["icoFaviconMissingSizes"] = 19] = "icoFaviconMissingSizes";
32
+ MessageId[MessageId["icoFaviconExpectedSizes"] = 20] = "icoFaviconExpectedSizes";
33
+ MessageId[MessageId["noDesktopPngFavicon"] = 21] = "noDesktopPngFavicon";
34
+ MessageId[MessageId["no96x96DesktopPngFavicon"] = 22] = "no96x96DesktopPngFavicon";
35
+ MessageId[MessageId["desktopPngFaviconDeclared"] = 23] = "desktopPngFaviconDeclared";
36
+ MessageId[MessageId["noDesktopPngFaviconHref"] = 24] = "noDesktopPngFaviconHref";
37
+ MessageId[MessageId["desktopPngFaviconCannotGet"] = 25] = "desktopPngFaviconCannotGet";
38
+ MessageId[MessageId["desktopPngFaviconDownloadable"] = 26] = "desktopPngFaviconDownloadable";
39
+ MessageId[MessageId["desktopPngFavicon404"] = 27] = "desktopPngFavicon404";
40
+ MessageId[MessageId["desktopPngFaviconWrongSize"] = 28] = "desktopPngFaviconWrongSize";
41
+ MessageId[MessageId["desktopPngFaviconRightSize"] = 29] = "desktopPngFaviconRightSize";
42
+ MessageId[MessageId["noTouchWebAppTitle"] = 30] = "noTouchWebAppTitle";
43
+ MessageId[MessageId["multipleTouchWebAppTitles"] = 31] = "multipleTouchWebAppTitles";
44
+ MessageId[MessageId["emptyTouchWebAppTitle"] = 32] = "emptyTouchWebAppTitle";
45
+ MessageId[MessageId["touchWebAppTitleDeclared"] = 33] = "touchWebAppTitleDeclared";
46
+ MessageId[MessageId["noTouchIcon"] = 34] = "noTouchIcon";
47
+ MessageId[MessageId["duplicatedTouchIconSizes"] = 35] = "duplicatedTouchIconSizes";
48
+ MessageId[MessageId["touchIconWithSize"] = 36] = "touchIconWithSize";
49
+ MessageId[MessageId["touchIconDeclared"] = 37] = "touchIconDeclared";
50
+ MessageId[MessageId["noTouchIconHref"] = 38] = "noTouchIconHref";
51
+ MessageId[MessageId["touchIcon404"] = 39] = "touchIcon404";
52
+ MessageId[MessageId["touchIconCannotGet"] = 40] = "touchIconCannotGet";
53
+ MessageId[MessageId["touchIconDownloadable"] = 41] = "touchIconDownloadable";
54
+ MessageId[MessageId["touchIconSquare"] = 42] = "touchIconSquare";
55
+ MessageId[MessageId["touchIcon180x180"] = 43] = "touchIcon180x180";
56
+ MessageId[MessageId["touchIconNotSquare"] = 44] = "touchIconNotSquare";
57
+ MessageId[MessageId["touchIconWrongSize"] = 45] = "touchIconWrongSize";
58
+ MessageId[MessageId["noManifest"] = 46] = "noManifest";
59
+ MessageId[MessageId["noManifestHref"] = 47] = "noManifestHref";
60
+ MessageId[MessageId["manifest404"] = 48] = "manifest404";
61
+ MessageId[MessageId["manifestCannotGet"] = 49] = "manifestCannotGet";
62
+ MessageId[MessageId["manifestInvalidJson"] = 50] = "manifestInvalidJson";
63
+ MessageId[MessageId["manifestName"] = 51] = "manifestName";
64
+ MessageId[MessageId["noManifestName"] = 52] = "noManifestName";
65
+ MessageId[MessageId["manifestShortName"] = 53] = "manifestShortName";
66
+ MessageId[MessageId["noManifestShortName"] = 54] = "noManifestShortName";
67
+ MessageId[MessageId["manifestBackgroundColor"] = 55] = "manifestBackgroundColor";
68
+ MessageId[MessageId["noManifestBackgroundColor"] = 56] = "noManifestBackgroundColor";
69
+ MessageId[MessageId["manifestThemeColor"] = 57] = "manifestThemeColor";
70
+ MessageId[MessageId["noManifestThemeColor"] = 58] = "noManifestThemeColor";
71
+ MessageId[MessageId["noManifestIcons"] = 59] = "noManifestIcons";
72
+ MessageId[MessageId["noManifestIcon"] = 60] = "noManifestIcon";
73
+ MessageId[MessageId["manifestIconDeclared"] = 61] = "manifestIconDeclared";
74
+ MessageId[MessageId["manifestIconCannotGet"] = 62] = "manifestIconCannotGet";
75
+ MessageId[MessageId["manifestIconDownloadable"] = 63] = "manifestIconDownloadable";
76
+ MessageId[MessageId["manifestIcon404"] = 64] = "manifestIcon404";
77
+ MessageId[MessageId["manifestIconNoHref"] = 65] = "manifestIconNoHref";
78
+ MessageId[MessageId["manifestIconNotSquare"] = 66] = "manifestIconNotSquare";
79
+ MessageId[MessageId["manifestIconRightSize"] = 67] = "manifestIconRightSize";
80
+ MessageId[MessageId["manifestIconSquare"] = 68] = "manifestIconSquare";
81
+ MessageId[MessageId["manifestIconWrongSize"] = 69] = "manifestIconWrongSize";
82
+ MessageId[MessageId["googleNoRobotsFile"] = 70] = "googleNoRobotsFile";
83
+ MessageId[MessageId["googleRobotsFileFound"] = 71] = "googleRobotsFileFound";
84
+ MessageId[MessageId["googleIcoBlockedByRobots"] = 72] = "googleIcoBlockedByRobots";
85
+ MessageId[MessageId["googleIcoAllowedByRobots"] = 73] = "googleIcoAllowedByRobots";
86
+ MessageId[MessageId["googleSvgIconBlockedByRobots"] = 74] = "googleSvgIconBlockedByRobots";
87
+ MessageId[MessageId["googleSvgIconAllowedByRobots"] = 75] = "googleSvgIconAllowedByRobots";
88
+ MessageId[MessageId["googlePngIconBlockedByRobots"] = 76] = "googlePngIconBlockedByRobots";
89
+ MessageId[MessageId["googlePngIconAllowedByRobots"] = 77] = "googlePngIconAllowedByRobots";
89
90
  })(MessageId || (exports.MessageId = MessageId = {}));
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realfavicongenerator/check-favicon",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Check the favicon of a website",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,5 +40,5 @@
40
40
  "robots-parser": "^3.0.1",
41
41
  "sharp": "^0.32.6"
42
42
  },
43
- "gitHead": "f0e219642ecea42c14a567ba6d64bf639871974c"
43
+ "gitHead": "25d06206eedd021db005448c6b2cf015f6cfc3cf"
44
44
  }
@@ -0,0 +1,280 @@
1
+ import { parse } from 'node-html-parser'
2
+ import { checkIcoFavicon } from "./ico";
3
+ import { CheckerMessage, CheckerStatus, DesktopSingleReport, FetchResponse, MessageId } from '../types';
4
+ import { filePathToReadableStream } from '../helper';
5
+ import { testFetcher } from '../test-helper';
6
+
7
+ type TestOutput = {
8
+ messages: Pick<CheckerMessage, 'id' | 'status'>[];
9
+ icon: DesktopSingleReport['icon'];
10
+ }
11
+
12
+ const runIcoTest = async (
13
+ headFragment: string | null,
14
+ output: TestOutput,
15
+ fetchDatabase: { [url: string]: FetchResponse } = {},
16
+ checkContent = true
17
+ ) => {
18
+ const root = headFragment ? parse(headFragment) : null;
19
+ const result = await checkIcoFavicon('https://example.com/', root, testFetcher(fetchDatabase));
20
+ const filteredMessages = result.messages.map(m => ({ status: m.status, id: m.id }));
21
+ expect(filteredMessages).toEqual(output.messages);
22
+
23
+ // Check icon properties - icon is always returned by checkIcoFavicon
24
+ const resultIcon = result.icon!;
25
+ const outputIcon = output.icon!;
26
+
27
+ expect(resultIcon.url).toEqual(outputIcon.url);
28
+ expect(resultIcon.width).toEqual(outputIcon.width);
29
+ expect(resultIcon.height).toEqual(outputIcon.height);
30
+
31
+ // For content, just check if it's null or not null unless exact match is needed
32
+ if (checkContent && outputIcon.content === null) {
33
+ expect(resultIcon.content).toBeNull();
34
+ } else if (checkContent && outputIcon.content !== null) {
35
+ expect(resultIcon.content).not.toBeNull();
36
+ expect(resultIcon.content).toMatch(/^data:image\/(png|bmp);base64,/);
37
+ }
38
+ }
39
+
40
+ test('checkIcoFavicon - noHead', async () => {
41
+ await runIcoTest(null, {
42
+ messages: [{
43
+ status: CheckerStatus.Error,
44
+ id: MessageId.noHead,
45
+ }],
46
+ icon: {
47
+ content: null,
48
+ url: null,
49
+ width: null,
50
+ height: null,
51
+ }
52
+ });
53
+ })
54
+
55
+ test('checkIcoFavicon - noIcoFavicon', async () => {
56
+ await runIcoTest(`<title>Some text</title>`, {
57
+ messages: [{
58
+ status: CheckerStatus.Error,
59
+ id: MessageId.noIcoFavicon,
60
+ }],
61
+ icon: {
62
+ content: null,
63
+ url: null,
64
+ width: null,
65
+ height: null,
66
+ }
67
+ });
68
+ })
69
+
70
+ test('checkIcoFavicon - implicit /favicon.ico when not declared', async () => {
71
+ const testIconPath = './fixtures/simple-ico.ico';
72
+
73
+ await runIcoTest(`<title>Some text</title>`, {
74
+ messages: [{
75
+ status: CheckerStatus.Ok,
76
+ id: MessageId.icoFaviconImplicitInRoot,
77
+ },{
78
+ status: CheckerStatus.Ok,
79
+ id: MessageId.icoFaviconDownloadable,
80
+ }, {
81
+ status: CheckerStatus.Ok,
82
+ id: MessageId.icoFaviconExpectedSizes,
83
+ }],
84
+ icon: {
85
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
86
+ url: 'https://example.com/favicon.ico',
87
+ width: 48,
88
+ height: 48,
89
+ }
90
+ }, {
91
+ 'https://example.com/favicon.ico': {
92
+ status: 200,
93
+ contentType: 'image/x-icon',
94
+ readableStream: await filePathToReadableStream(testIconPath)
95
+ }
96
+ });
97
+ })
98
+
99
+ test('checkIcoFavicon - multipleIcoFavicons with shortcut icon', async () => {
100
+ await runIcoTest(`
101
+ <link rel="shortcut icon" href="/favicon1.ico" />
102
+ <link rel="shortcut icon" href="/favicon2.ico" />
103
+ `, {
104
+ messages: [{
105
+ status: CheckerStatus.Error,
106
+ id: MessageId.multipleIcoFavicons,
107
+ }],
108
+ icon: {
109
+ content: null,
110
+ url: null,
111
+ width: null,
112
+ height: null,
113
+ }
114
+ });
115
+ })
116
+
117
+ test('checkIcoFavicon - multipleIcoFavicons with type="image/x-icon"', async () => {
118
+ await runIcoTest(`
119
+ <link rel="icon" type="image/x-icon" href="/favicon1.ico" />
120
+ <link rel="icon" type="image/x-icon" href="/favicon2.ico" />
121
+ `, {
122
+ messages: [{
123
+ status: CheckerStatus.Error,
124
+ id: MessageId.multipleIcoFavicons,
125
+ }],
126
+ icon: {
127
+ content: null,
128
+ url: null,
129
+ width: null,
130
+ height: null,
131
+ }
132
+ });
133
+ })
134
+
135
+ test('checkIcoFavicon - icoFaviconDeclared & noIcoFaviconHref', async () => {
136
+ await runIcoTest(`<link rel="shortcut icon" />`, {
137
+ messages: [{
138
+ status: CheckerStatus.Ok,
139
+ id: MessageId.icoFaviconDeclared,
140
+ }, {
141
+ status: CheckerStatus.Error,
142
+ id: MessageId.noIcoFaviconHref,
143
+ }],
144
+ icon: {
145
+ content: null,
146
+ url: null,
147
+ width: null,
148
+ height: null,
149
+ }
150
+ });
151
+ })
152
+
153
+ test('checkIcoFavicon - icoFaviconDeclared & icoFavicon404', async () => {
154
+ await runIcoTest(`<link rel="shortcut icon" href="/favicon.ico" />`, {
155
+ messages: [{
156
+ status: CheckerStatus.Ok,
157
+ id: MessageId.icoFaviconDeclared,
158
+ }, {
159
+ status: CheckerStatus.Error,
160
+ id: MessageId.icoFavicon404,
161
+ }],
162
+ icon: {
163
+ content: null,
164
+ url: 'https://example.com/favicon.ico',
165
+ width: null,
166
+ height: null,
167
+ }
168
+ });
169
+ })
170
+
171
+ test('checkIcoFavicon - icoFaviconDeclared & icoFaviconCannotGet', async () => {
172
+ await runIcoTest(`<link rel="shortcut icon" href="/favicon.ico" />`, {
173
+ messages: [{
174
+ status: CheckerStatus.Ok,
175
+ id: MessageId.icoFaviconDeclared,
176
+ }, {
177
+ status: CheckerStatus.Error,
178
+ id: MessageId.icoFaviconCannotGet,
179
+ }],
180
+ icon: {
181
+ content: null,
182
+ url: 'https://example.com/favicon.ico',
183
+ width: null,
184
+ height: null,
185
+ }
186
+ }, {
187
+ 'https://example.com/favicon.ico': {
188
+ status: 403,
189
+ contentType: 'image/x-icon'
190
+ }
191
+ });
192
+ })
193
+
194
+ test('checkIcoFavicon - icoFaviconDeclared & icoFaviconDownloadable & icoFaviconExpectedSizes', async () => {
195
+ const testIconPath = './fixtures/simple-ico.ico';
196
+
197
+ await runIcoTest(`<link rel="shortcut icon" href="/favicon.ico" />`, {
198
+ messages: [{
199
+ status: CheckerStatus.Ok,
200
+ id: MessageId.icoFaviconDeclared,
201
+ }, {
202
+ status: CheckerStatus.Ok,
203
+ id: MessageId.icoFaviconDownloadable,
204
+ }, {
205
+ status: CheckerStatus.Ok,
206
+ id: MessageId.icoFaviconExpectedSizes,
207
+ }],
208
+ icon: {
209
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
210
+ url: 'https://example.com/favicon.ico',
211
+ width: 48,
212
+ height: 48,
213
+ }
214
+ }, {
215
+ 'https://example.com/favicon.ico': {
216
+ status: 200,
217
+ contentType: 'image/x-icon',
218
+ readableStream: await filePathToReadableStream(testIconPath)
219
+ }
220
+ });
221
+ })
222
+
223
+ test('checkIcoFavicon - using type="image/x-icon"', async () => {
224
+ const testIconPath = './fixtures/simple-ico.ico';
225
+
226
+ await runIcoTest(`<link rel="icon" type="image/x-icon" href="/favicon.ico" />`, {
227
+ messages: [{
228
+ status: CheckerStatus.Ok,
229
+ id: MessageId.icoFaviconDeclared,
230
+ }, {
231
+ status: CheckerStatus.Ok,
232
+ id: MessageId.icoFaviconDownloadable,
233
+ }, {
234
+ status: CheckerStatus.Ok,
235
+ id: MessageId.icoFaviconExpectedSizes,
236
+ }],
237
+ icon: {
238
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
239
+ url: 'https://example.com/favicon.ico',
240
+ width: 48,
241
+ height: 48,
242
+ }
243
+ }, {
244
+ 'https://example.com/favicon.ico': {
245
+ status: 200,
246
+ contentType: 'image/x-icon',
247
+ readableStream: await filePathToReadableStream(testIconPath)
248
+ }
249
+ });
250
+ })
251
+
252
+ // For https://github.com/RealFaviconGenerator/core/issues/2
253
+ test('checkIcoFavicon - Protocol-relative URL', async () => {
254
+ const testIconPath = './fixtures/simple-ico.ico';
255
+
256
+ await runIcoTest(`<link rel="shortcut icon" href="//example.com/favicon.ico" />`, {
257
+ messages: [{
258
+ status: CheckerStatus.Ok,
259
+ id: MessageId.icoFaviconDeclared,
260
+ }, {
261
+ status: CheckerStatus.Ok,
262
+ id: MessageId.icoFaviconDownloadable,
263
+ }, {
264
+ status: CheckerStatus.Ok,
265
+ id: MessageId.icoFaviconExpectedSizes,
266
+ }],
267
+ icon: {
268
+ content: "data:image/png;base64,placeholder", // Will be checked for format only
269
+ url: 'https://example.com/favicon.ico',
270
+ width: 48,
271
+ height: 48,
272
+ }
273
+ }, {
274
+ 'https://example.com/favicon.ico': {
275
+ status: 200,
276
+ contentType: 'image/x-icon',
277
+ readableStream: await filePathToReadableStream(testIconPath)
278
+ }
279
+ });
280
+ })
@@ -28,20 +28,16 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
28
28
 
29
29
  let iconUrl: string | null = null;
30
30
  let images;
31
+ let isDeclared = false;
31
32
 
32
- if (icos.length === 0) {
33
- messages.push({
34
- status: CheckerStatus.Error,
35
- id: MessageId.noIcoFavicon,
36
- text: 'There is no ICO favicon'
37
- });
38
- } else if (icos.length > 1) {
33
+ if (icos.length > 1) {
39
34
  messages.push({
40
35
  status: CheckerStatus.Error,
41
36
  id: MessageId.multipleIcoFavicons,
42
37
  text: `There are ${icos.length} ICO favicons`
43
38
  });
44
- } else {
39
+ } else if (icos.length === 1) {
40
+ isDeclared = true;
45
41
  messages.push({
46
42
  status: CheckerStatus.Ok,
47
43
  id: MessageId.icoFaviconDeclared,
@@ -57,63 +53,97 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
57
53
  });
58
54
  } else {
59
55
  iconUrl = mergeUrlAndPath(url, href);
60
- const iconResponse = await fetcher(iconUrl, 'image/x-icon');
61
- if (iconResponse.status === 404) {
56
+ }
57
+ } else {
58
+ // No declared ICO favicon, try the implicit /favicon.ico convention
59
+ iconUrl = mergeUrlAndPath(url, '/favicon.ico');
60
+ }
61
+
62
+ // If we have an iconUrl (either from declaration or implicit), try to fetch it
63
+ if (iconUrl) {
64
+ const iconResponse = await fetcher(iconUrl, 'image/x-icon');
65
+ if (iconResponse.status === 404) {
66
+ if (isDeclared) {
62
67
  messages.push({
63
68
  status: CheckerStatus.Error,
64
69
  id: MessageId.icoFavicon404,
65
70
  text: `ICO favicon not found at ${iconUrl}`
66
71
  });
67
- } else if (iconResponse.status >= 300 || !iconResponse.readableStream) {
72
+ } else {
73
+ // Implicit favicon not found, report no ICO favicon
74
+ messages.push({
75
+ status: CheckerStatus.Error,
76
+ id: MessageId.noIcoFavicon,
77
+ text: 'There is no ICO favicon'
78
+ });
79
+ iconUrl = null;
80
+ }
81
+ } else if (iconResponse.status >= 300 || !iconResponse.readableStream) {
82
+ if (isDeclared) {
68
83
  messages.push({
69
84
  status: CheckerStatus.Error,
70
85
  id: MessageId.icoFaviconCannotGet,
71
86
  text: `Error fetching ICO favicon at ${iconUrl} (status ${iconResponse.status})`
72
87
  });
73
88
  } else {
89
+ // Implicit favicon cannot be fetched, report no ICO favicon
90
+ messages.push({
91
+ status: CheckerStatus.Error,
92
+ id: MessageId.noIcoFavicon,
93
+ text: 'There is no ICO favicon'
94
+ });
95
+ iconUrl = null;
96
+ }
97
+ } else {
98
+ if (!isDeclared) {
74
99
  messages.push({
75
100
  status: CheckerStatus.Ok,
76
- id: MessageId.icoFaviconDownloadable,
77
- text: 'ICO favicon found'
101
+ id: MessageId.icoFaviconImplicitInRoot,
102
+ text: 'An implicit ICO favicon is found at /favicon.ico'
78
103
  });
104
+ }
105
+
106
+ messages.push({
107
+ status: CheckerStatus.Ok,
108
+ id: MessageId.icoFaviconDownloadable,
109
+ text: 'ICO favicon found'
110
+ });
111
+
112
+ const iconBuffer = await readableStreamToBuffer(iconResponse.readableStream);
113
+ images = decodeIco(iconBuffer);
114
+
115
+ const imageSizes = images.map(image => `${image.width}x${image.height}`);
116
+
117
+ const expectedSizes = IcoFaviconSizes.map(size => `${size}x${size}`);
118
+
119
+ const extraSizes = imageSizes.filter(size => !expectedSizes.includes(size));
120
+ if (extraSizes.length > 0) {
121
+ messages.push({
122
+ status: CheckerStatus.Warning,
123
+ id: MessageId.icoFaviconExtraSizes,
124
+ text: `Extra sizes found in ICO favicon: ${extraSizes.join(', ')}`
125
+ });
126
+ }
79
127
 
80
- const iconBuffer = await readableStreamToBuffer(iconResponse.readableStream);
81
- images = await decodeIco(iconBuffer);
82
-
83
- const imageSizes = images.map(image => `${image.width}x${image.height}`);
84
-
85
- const expectedSizes = IcoFaviconSizes.map(size => `${size}x${size}`);
86
-
87
- const extraSizes = imageSizes.filter(size => !expectedSizes.includes(size));
88
- if (extraSizes.length > 0) {
89
- messages.push({
90
- status: CheckerStatus.Warning,
91
- id: MessageId.icoFaviconExtraSizes,
92
- text: `Extra sizes found in ICO favicon: ${extraSizes.join(', ')}`
93
- });
94
- }
95
-
96
- const missingSizes = expectedSizes.filter(size => !imageSizes.includes(size));
97
- if (missingSizes.length > 0) {
98
- messages.push({
99
- status: CheckerStatus.Warning,
100
- id: MessageId.icoFaviconMissingSizes,
101
- text: `Missing sizes in ICO favicon: ${missingSizes.join(', ')}`
102
- });
103
- }
104
-
105
- if (extraSizes.length === 0 && missingSizes.length === 0) {
106
- messages.push({
107
- status: CheckerStatus.Ok,
108
- id: MessageId.icoFaviconExpectedSizes,
109
- text: `The ICO favicon has the expected sizes (${imageSizes.join(', ')})`
110
- });
111
- }
128
+ const missingSizes = expectedSizes.filter(size => !imageSizes.includes(size));
129
+ if (missingSizes.length > 0) {
130
+ messages.push({
131
+ status: CheckerStatus.Warning,
132
+ id: MessageId.icoFaviconMissingSizes,
133
+ text: `Missing sizes in ICO favicon: ${missingSizes.join(', ')}`
134
+ });
135
+ }
136
+
137
+ if (extraSizes.length === 0 && missingSizes.length === 0) {
138
+ messages.push({
139
+ status: CheckerStatus.Ok,
140
+ id: MessageId.icoFaviconExpectedSizes,
141
+ text: `The ICO favicon has the expected sizes (${imageSizes.join(', ')})`
142
+ });
112
143
  }
113
144
  }
114
145
  }
115
146
 
116
- let content: string | null = null;
117
147
  const theIcon: CheckedIcon = {
118
148
  content: null,
119
149
  url: iconUrl,
@@ -123,7 +153,7 @@ export const checkIcoFavicon = async (url: string, head: HTMLElement | null, fet
123
153
  if (images) {
124
154
  const image = images[0];
125
155
  const mimeType = (image.type === "bmp") ? "image/bmp" : "image/png";
126
- theIcon.content = await bufferToDataUrl(Buffer.from(image.data), mimeType);
156
+ theIcon.content = bufferToDataUrl(Buffer.from(image.data.buffer, image.data.byteOffset, image.data.byteLength), mimeType);
127
157
  theIcon.width = image.width;
128
158
  theIcon.height = image.height;
129
159
  }
package/src/types.ts CHANGED
@@ -21,6 +21,7 @@ export enum MessageId {
21
21
  noIcoFavicon,
22
22
  multipleIcoFavicons,
23
23
  icoFaviconDeclared,
24
+ icoFaviconImplicitInRoot,
24
25
  noIcoFaviconHref,
25
26
  icoFavicon404,
26
27
  icoFaviconCannotGet,