@realfavicongenerator/check-favicon 0.0.1 → 0.1.0
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/README.md +26 -0
- package/dist/check.d.ts +3 -0
- package/dist/check.js +27 -0
- package/dist/desktop/desktop.d.ts +7 -0
- package/dist/desktop/desktop.js +213 -0
- package/dist/desktop/desktop.test.d.ts +1 -0
- package/dist/desktop/desktop.test.js +97 -0
- package/dist/desktop/ico.d.ts +4 -0
- package/dist/desktop/ico.js +115 -0
- package/dist/helper.d.ts +23 -0
- package/dist/helper.js +189 -0
- package/dist/helper.test.d.ts +1 -0
- package/dist/helper.test.js +121 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +15 -0
- package/dist/test-helper.d.ts +4 -0
- package/dist/test-helper.js +22 -0
- package/dist/touch-icon.d.ts +6 -0
- package/dist/touch-icon.js +187 -0
- package/dist/touch-icon.test.d.ts +1 -0
- package/dist/touch-icon.test.js +195 -0
- package/dist/types.d.ts +113 -0
- package/dist/types.js +81 -0
- package/dist/web-manifest.d.ts +4 -0
- package/dist/web-manifest.js +262 -0
- package/dist/web-manifest.test.d.ts +1 -0
- package/dist/web-manifest.test.js +172 -0
- package/package.json +6 -3
- package/src/check.ts +18 -0
- package/src/desktop/desktop.test.ts +1 -1
- package/src/desktop/desktop.ts +2 -2
- package/src/desktop/ico.ts +1 -1
- package/src/helper.ts +33 -1
- package/src/index.ts +16 -160
- package/src/test-helper.ts +1 -1
- package/src/touch-icon.test.ts +1 -1
- package/src/touch-icon.ts +2 -2
- package/src/types.ts +129 -0
- package/src/web-manifest.test.ts +3 -3
- package/src/web-manifest.ts +3 -3
- package/tsconfig.json +2 -2
package/README.md
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# check-favicon
|
|
2
|
+
|
|
3
|
+
A TypeSCript library to check the favicon of a website.
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @realfavicongenerator/check-favicon node-html-parser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { parse } from 'node-html-parser'
|
|
15
|
+
|
|
16
|
+
const body = fs.readFileSync('some_page.html');
|
|
17
|
+
|
|
18
|
+
const root = parse(body);
|
|
19
|
+
const head = root.querySelector('head');
|
|
20
|
+
|
|
21
|
+
const desktopFaviconReport = await checkDesktopFavicon(pageUrl, head);
|
|
22
|
+
const touchIconFaviconReport = await checkTouchIcon(pageUrl, head);
|
|
23
|
+
const webAppManifestFaviconReport = await checkWebManifest(pageUrl, head);
|
|
24
|
+
|
|
25
|
+
console.log("Analysis and icons", desktopFaviconReport, touchIconFaviconReport, webAppManifestFaviconReport);
|
|
26
|
+
```
|
package/dist/check.d.ts
ADDED
package/dist/check.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
exports.checkFavicon = void 0;
|
|
13
|
+
const desktop_1 = require("./desktop/desktop");
|
|
14
|
+
const helper_1 = require("./helper");
|
|
15
|
+
const touch_icon_1 = require("./touch-icon");
|
|
16
|
+
const web_manifest_1 = require("./web-manifest");
|
|
17
|
+
const checkFavicon = (baseUrl_1, head_1, ...args_1) => __awaiter(void 0, [baseUrl_1, head_1, ...args_1], void 0, function* (baseUrl, head, fetcher = helper_1.fetchFetcher) {
|
|
18
|
+
const desktop = yield (0, desktop_1.checkDesktopFavicon)(baseUrl, head, fetcher);
|
|
19
|
+
const touchIcon = yield (0, touch_icon_1.checkTouchIcon)(baseUrl, head, fetcher);
|
|
20
|
+
const webAppManifest = yield (0, web_manifest_1.checkWebAppManifest)(baseUrl, head, fetcher);
|
|
21
|
+
return {
|
|
22
|
+
desktop,
|
|
23
|
+
touchIcon,
|
|
24
|
+
webAppManifest
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
exports.checkFavicon = checkFavicon;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CheckerMessage, DesktopFaviconReport, Fetcher } from "../types";
|
|
2
|
+
import { HTMLElement } from 'node-html-parser';
|
|
3
|
+
export declare const PngFaviconFileSize = 48;
|
|
4
|
+
export declare const checkSvgFavicon: (baseUrl: string, head: HTMLElement | null, fetcher?: Fetcher) => Promise<CheckerMessage[]>;
|
|
5
|
+
export declare const checkSvgFaviconFile: (baseUrl: string, url: string, fetcher: Fetcher) => Promise<CheckerMessage[]>;
|
|
6
|
+
export declare const checkPngFavicon: (baseUrl: string, head: HTMLElement | null, fetcher?: Fetcher) => Promise<DesktopFaviconReport>;
|
|
7
|
+
export declare const checkDesktopFavicon: (baseUrl: string, head: HTMLElement | null, fetcher?: Fetcher) => Promise<DesktopFaviconReport>;
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.checkDesktopFavicon = exports.checkPngFavicon = exports.checkSvgFaviconFile = exports.checkSvgFavicon = exports.PngFaviconFileSize = void 0;
|
|
16
|
+
const types_1 = require("../types");
|
|
17
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
18
|
+
const helper_1 = require("../helper");
|
|
19
|
+
const ico_1 = require("./ico");
|
|
20
|
+
exports.PngFaviconFileSize = 48;
|
|
21
|
+
const checkSvgFavicon = (baseUrl_1, head_1, ...args_1) => __awaiter(void 0, [baseUrl_1, head_1, ...args_1], void 0, function* (baseUrl, head, fetcher = helper_1.fetchFetcher) {
|
|
22
|
+
const messages = [];
|
|
23
|
+
if (!head) {
|
|
24
|
+
messages.push({
|
|
25
|
+
status: types_1.CheckerStatus.Error,
|
|
26
|
+
id: types_1.MessageId.noHead,
|
|
27
|
+
text: 'No <head> element'
|
|
28
|
+
});
|
|
29
|
+
return messages;
|
|
30
|
+
}
|
|
31
|
+
const svgs = head === null || head === void 0 ? void 0 : head.querySelectorAll("link[rel='icon'][type='image/svg+xml']");
|
|
32
|
+
if (svgs.length === 0) {
|
|
33
|
+
messages.push({
|
|
34
|
+
status: types_1.CheckerStatus.Error,
|
|
35
|
+
id: types_1.MessageId.noSvgFavicon,
|
|
36
|
+
text: 'There is no SVG favicon'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (svgs.length > 1) {
|
|
40
|
+
messages.push({
|
|
41
|
+
status: types_1.CheckerStatus.Error,
|
|
42
|
+
id: types_1.MessageId.multipleSvgFavicons,
|
|
43
|
+
text: `There are ${svgs.length} SVG favicons`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
messages.push({
|
|
48
|
+
status: types_1.CheckerStatus.Ok,
|
|
49
|
+
id: types_1.MessageId.svgFaviconDeclared,
|
|
50
|
+
text: 'The SVG favicon is declared'
|
|
51
|
+
});
|
|
52
|
+
const href = svgs[0].attributes.href;
|
|
53
|
+
if (!href) {
|
|
54
|
+
messages.push({
|
|
55
|
+
status: types_1.CheckerStatus.Error,
|
|
56
|
+
id: types_1.MessageId.noSvgFaviconHref,
|
|
57
|
+
text: 'The SVG markup has no href attribute'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const iconMessages = yield (0, exports.checkSvgFaviconFile)(baseUrl, href, fetcher);
|
|
62
|
+
return [...messages, ...iconMessages];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return messages;
|
|
66
|
+
});
|
|
67
|
+
exports.checkSvgFavicon = checkSvgFavicon;
|
|
68
|
+
const checkSvgFaviconFile = (baseUrl, url, fetcher) => __awaiter(void 0, void 0, void 0, function* () {
|
|
69
|
+
const messages = [];
|
|
70
|
+
const svgUrl = (0, helper_1.mergeUrlAndPath)(baseUrl, url);
|
|
71
|
+
const res = yield fetcher(svgUrl, 'image/svg+xml');
|
|
72
|
+
if (res.status === 404) {
|
|
73
|
+
messages.push({
|
|
74
|
+
status: types_1.CheckerStatus.Error,
|
|
75
|
+
id: types_1.MessageId.svgFavicon404,
|
|
76
|
+
text: `The SVG icon file \`${url}\` does not exist (404 error)`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else if (res.status >= 300) {
|
|
80
|
+
messages.push({
|
|
81
|
+
status: types_1.CheckerStatus.Error,
|
|
82
|
+
id: types_1.MessageId.svgFaviconCannotGet,
|
|
83
|
+
text: `Cannot get the SVG icon file at \`${url}\` (${res.status} error)`
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else if (res.readableStream) {
|
|
87
|
+
messages.push({
|
|
88
|
+
status: types_1.CheckerStatus.Ok,
|
|
89
|
+
id: types_1.MessageId.svgFaviconDownloadable,
|
|
90
|
+
text: `The SVG favicon is accessible at \`${url}\``
|
|
91
|
+
});
|
|
92
|
+
const content = yield (0, helper_1.readableStreamToString)(res.readableStream);
|
|
93
|
+
const meta = yield (0, sharp_1.default)(Buffer.from(content)).metadata();
|
|
94
|
+
if (meta.width !== meta.height) {
|
|
95
|
+
messages.push({
|
|
96
|
+
status: types_1.CheckerStatus.Error,
|
|
97
|
+
id: types_1.MessageId.svgFaviconNotSquare,
|
|
98
|
+
text: `The SVG is not square (${meta.width}x${meta.height})`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
messages.push({
|
|
103
|
+
status: types_1.CheckerStatus.Ok,
|
|
104
|
+
id: types_1.MessageId.svgFaviconSquare,
|
|
105
|
+
text: `The SVG is square (${meta.width}x${meta.height})`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return messages;
|
|
110
|
+
});
|
|
111
|
+
exports.checkSvgFaviconFile = checkSvgFaviconFile;
|
|
112
|
+
const checkPngFavicon = (baseUrl_2, head_2, ...args_2) => __awaiter(void 0, [baseUrl_2, head_2, ...args_2], void 0, function* (baseUrl, head, fetcher = helper_1.fetchFetcher) {
|
|
113
|
+
const messages = [];
|
|
114
|
+
if (!head) {
|
|
115
|
+
messages.push({
|
|
116
|
+
status: types_1.CheckerStatus.Error,
|
|
117
|
+
id: types_1.MessageId.noHead,
|
|
118
|
+
text: 'No <head> element'
|
|
119
|
+
});
|
|
120
|
+
return { messages, icon: null };
|
|
121
|
+
}
|
|
122
|
+
const icons = head === null || head === void 0 ? void 0 : head.querySelectorAll("link[rel='icon'][type='image/png']");
|
|
123
|
+
if (icons.length === 0) {
|
|
124
|
+
messages.push({
|
|
125
|
+
status: types_1.CheckerStatus.Error,
|
|
126
|
+
id: types_1.MessageId.noDesktopPngFavicon,
|
|
127
|
+
text: 'There is no desktop PNG favicon'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const size = `${exports.PngFaviconFileSize}x${exports.PngFaviconFileSize}`;
|
|
132
|
+
const sizedIconMarkup = icons.filter(icon => icon.attributes.sizes === size);
|
|
133
|
+
if (sizedIconMarkup.length === 0) {
|
|
134
|
+
messages.push({
|
|
135
|
+
status: types_1.CheckerStatus.Error,
|
|
136
|
+
id: types_1.MessageId.no48x48DesktopPngFavicon,
|
|
137
|
+
text: `There is no ${size} desktop PNG favicon`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
messages.push({
|
|
142
|
+
status: types_1.CheckerStatus.Ok,
|
|
143
|
+
id: types_1.MessageId.desktopPngFaviconDeclared,
|
|
144
|
+
text: `The ${size} desktop PNG favicon is declared`
|
|
145
|
+
});
|
|
146
|
+
const href = icons[0].attributes.href;
|
|
147
|
+
if (!href) {
|
|
148
|
+
messages.push({
|
|
149
|
+
status: types_1.CheckerStatus.Error,
|
|
150
|
+
id: types_1.MessageId.noDesktopPngFaviconHref,
|
|
151
|
+
text: `The ${size} desktop favicon markup has no href attribute`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const iconUrl = (0, helper_1.mergeUrlAndPath)(baseUrl, href);
|
|
156
|
+
const processor = {
|
|
157
|
+
cannotGet: (httpStatus) => {
|
|
158
|
+
messages.push({
|
|
159
|
+
status: types_1.CheckerStatus.Error,
|
|
160
|
+
id: types_1.MessageId.desktopPngFaviconCannotGet,
|
|
161
|
+
text: `Cannot get the ${size} desktop PNG favicon at \`${iconUrl}\` (${httpStatus} error)`
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
downloadable: () => {
|
|
165
|
+
messages.push({
|
|
166
|
+
status: types_1.CheckerStatus.Ok,
|
|
167
|
+
id: types_1.MessageId.desktopPngFaviconDownloadable,
|
|
168
|
+
text: `The ${size} desktop PNG favicon is accessible`
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
icon404: () => {
|
|
172
|
+
messages.push({
|
|
173
|
+
status: types_1.CheckerStatus.Error,
|
|
174
|
+
id: types_1.MessageId.desktopPngFavicon404,
|
|
175
|
+
text: `The ${size} desktop PNG favicon does not exist (404 error)`
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
notSquare: (width, height) => { }, // Ignore this message
|
|
179
|
+
wrongSize: (widthHeight) => {
|
|
180
|
+
messages.push({
|
|
181
|
+
status: types_1.CheckerStatus.Error,
|
|
182
|
+
id: types_1.MessageId.desktopPngFaviconWrongSize,
|
|
183
|
+
text: `The ${size} desktop PNG favicon has the wrong size (${widthHeight}x${widthHeight})`
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
rightSize(widthHeight) {
|
|
187
|
+
messages.push({
|
|
188
|
+
status: types_1.CheckerStatus.Ok,
|
|
189
|
+
id: types_1.MessageId.desktopPngFaviconRightSize,
|
|
190
|
+
text: `The ${size} desktop PNG favicon has the right size`
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
square: (widthHeight) => { }, // Ignore this message,
|
|
194
|
+
noHref: () => { } // Ignore this message
|
|
195
|
+
};
|
|
196
|
+
const icon = yield (0, helper_1.checkIcon)(iconUrl, processor, fetcher, icons[0].attributes.mimeType || 'image/png', exports.PngFaviconFileSize);
|
|
197
|
+
return { messages, icon };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { messages, icon: null };
|
|
202
|
+
});
|
|
203
|
+
exports.checkPngFavicon = checkPngFavicon;
|
|
204
|
+
const checkDesktopFavicon = (baseUrl_3, head_3, ...args_3) => __awaiter(void 0, [baseUrl_3, head_3, ...args_3], void 0, function* (baseUrl, head, fetcher = helper_1.fetchFetcher) {
|
|
205
|
+
const svgMessages = yield (0, exports.checkSvgFavicon)(baseUrl, head, fetcher);
|
|
206
|
+
const pngReport = yield (0, exports.checkPngFavicon)(baseUrl, head, fetcher);
|
|
207
|
+
const icoReport = yield (0, ico_1.checkIcoFavicon)(baseUrl, head, fetcher);
|
|
208
|
+
return {
|
|
209
|
+
messages: [...svgMessages, ...pngReport.messages, ...icoReport],
|
|
210
|
+
icon: pngReport.icon
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
exports.checkDesktopFavicon = checkDesktopFavicon;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
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 desktop_1 = require("./desktop");
|
|
14
|
+
const types_1 = require("../types");
|
|
15
|
+
const helper_1 = require("../helper");
|
|
16
|
+
const test_helper_1 = require("../test-helper");
|
|
17
|
+
const runTest = (headFragment_1, output_1, ...args_1) => __awaiter(void 0, [headFragment_1, output_1, ...args_1], void 0, function* (headFragment, output, fetchDatabase = {}) {
|
|
18
|
+
const root = headFragment ? (0, node_html_parser_1.parse)(headFragment) : null;
|
|
19
|
+
const result = yield (0, desktop_1.checkSvgFavicon)('https://example.com/', root, (0, test_helper_1.testFetcher)(fetchDatabase));
|
|
20
|
+
const filteredMessages = result.map(m => ({ status: m.status, id: m.id }));
|
|
21
|
+
expect(filteredMessages).toEqual(output);
|
|
22
|
+
});
|
|
23
|
+
test('checkSvgFavicon - noHead', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
yield runTest(null, [{
|
|
25
|
+
status: types_1.CheckerStatus.Error,
|
|
26
|
+
id: types_1.MessageId.noHead,
|
|
27
|
+
}]);
|
|
28
|
+
}));
|
|
29
|
+
test('checkSvgFavicon - noSvgFavicon', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
+
yield runTest(`<title>SOme text</title>`, [{
|
|
31
|
+
status: types_1.CheckerStatus.Error,
|
|
32
|
+
id: types_1.MessageId.noSvgFavicon,
|
|
33
|
+
}]);
|
|
34
|
+
}));
|
|
35
|
+
test('checkSvgFavicon - multipleSvgFavicons', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
36
|
+
yield runTest(`
|
|
37
|
+
<link rel="icon" type="image/svg+xml" href="/the-icon.svg" />
|
|
38
|
+
<link rel="icon" type="image/svg+xml" href="/another-icon.svg" />
|
|
39
|
+
`, [{
|
|
40
|
+
status: types_1.CheckerStatus.Error,
|
|
41
|
+
id: types_1.MessageId.multipleSvgFavicons,
|
|
42
|
+
}]);
|
|
43
|
+
}));
|
|
44
|
+
test('checkSvgFavicon - svgFaviconDeclared & noSvgFaviconHref', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
yield runTest(`<link rel="icon" type="image/svg+xml" />`, [{
|
|
46
|
+
status: types_1.CheckerStatus.Ok,
|
|
47
|
+
id: types_1.MessageId.svgFaviconDeclared,
|
|
48
|
+
}, {
|
|
49
|
+
status: types_1.CheckerStatus.Error,
|
|
50
|
+
id: types_1.MessageId.noSvgFaviconHref,
|
|
51
|
+
}]);
|
|
52
|
+
}));
|
|
53
|
+
test('checkSvgFavicon - svgFaviconDeclared & svgFavicon404', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
yield runTest(`<link rel="icon" type="image/svg+xml" href="/the-icon.svg" />`, [{
|
|
55
|
+
status: types_1.CheckerStatus.Ok,
|
|
56
|
+
id: types_1.MessageId.svgFaviconDeclared,
|
|
57
|
+
}, {
|
|
58
|
+
status: types_1.CheckerStatus.Error,
|
|
59
|
+
id: types_1.MessageId.svgFavicon404,
|
|
60
|
+
}]);
|
|
61
|
+
}));
|
|
62
|
+
test('checkSvgFavicon - svgFaviconDeclared & svgFaviconCannotGet', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
yield runTest(`<link rel="icon" type="image/svg+xml" href="/the-icon.svg" />`, [{
|
|
64
|
+
status: types_1.CheckerStatus.Ok,
|
|
65
|
+
id: types_1.MessageId.svgFaviconDeclared,
|
|
66
|
+
}, {
|
|
67
|
+
status: types_1.CheckerStatus.Error,
|
|
68
|
+
id: types_1.MessageId.svgFaviconCannotGet,
|
|
69
|
+
}], {
|
|
70
|
+
'https://example.com/the-icon.svg': {
|
|
71
|
+
status: 403,
|
|
72
|
+
contentType: 'image/svg+xml'
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}));
|
|
76
|
+
test('checkSvgFavicon - svgFaviconDeclared & svgFaviconDownloadable & svgFaviconSquare', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
const testIconPath = './fixtures/happy-face.svg';
|
|
78
|
+
const serpIcon = yield (0, helper_1.filePathToString)(testIconPath);
|
|
79
|
+
yield runTest(`<link rel="icon" type="image/svg+xml" href="/the-icon.svg" />`, [
|
|
80
|
+
{
|
|
81
|
+
status: types_1.CheckerStatus.Ok,
|
|
82
|
+
id: types_1.MessageId.svgFaviconDeclared,
|
|
83
|
+
}, {
|
|
84
|
+
status: types_1.CheckerStatus.Ok,
|
|
85
|
+
id: types_1.MessageId.svgFaviconDownloadable,
|
|
86
|
+
}, {
|
|
87
|
+
status: types_1.CheckerStatus.Ok,
|
|
88
|
+
id: types_1.MessageId.svgFaviconSquare,
|
|
89
|
+
}
|
|
90
|
+
], {
|
|
91
|
+
'https://example.com/the-icon.svg': {
|
|
92
|
+
status: 200,
|
|
93
|
+
contentType: 'image/svg+xml',
|
|
94
|
+
readableStream: yield (0, helper_1.filePathToReadableStream)(testIconPath)
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CheckerMessage, Fetcher } from "../types";
|
|
2
|
+
import { HTMLElement } from 'node-html-parser';
|
|
3
|
+
export declare const IcoFaviconSizes: number[];
|
|
4
|
+
export declare const checkIcoFavicon: (url: string, head: HTMLElement | null, fetcher: Fetcher) => Promise<CheckerMessage[]>;
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.checkIcoFavicon = exports.IcoFaviconSizes = void 0;
|
|
16
|
+
const types_1 = require("../types");
|
|
17
|
+
const helper_1 = require("../helper");
|
|
18
|
+
const decode_ico_1 = __importDefault(require("decode-ico"));
|
|
19
|
+
exports.IcoFaviconSizes = [48, 32, 16];
|
|
20
|
+
const checkIcoFavicon = (url, head, fetcher) => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
+
const messages = [];
|
|
22
|
+
if (!head) {
|
|
23
|
+
messages.push({
|
|
24
|
+
status: types_1.CheckerStatus.Error,
|
|
25
|
+
id: types_1.MessageId.noHead,
|
|
26
|
+
text: 'No <head> element'
|
|
27
|
+
});
|
|
28
|
+
return messages;
|
|
29
|
+
}
|
|
30
|
+
const icos = head.querySelectorAll('link[rel="shortcut icon"]') ||
|
|
31
|
+
head.querySelectorAll('link[rel="icon"][type="image/x-icon"]');
|
|
32
|
+
if (icos.length === 0) {
|
|
33
|
+
messages.push({
|
|
34
|
+
status: types_1.CheckerStatus.Error,
|
|
35
|
+
id: types_1.MessageId.noIcoFavicon,
|
|
36
|
+
text: 'There is no ICO favicon'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (icos.length > 1) {
|
|
40
|
+
messages.push({
|
|
41
|
+
status: types_1.CheckerStatus.Error,
|
|
42
|
+
id: types_1.MessageId.multipleIcoFavicons,
|
|
43
|
+
text: `There are ${icos.length} ICO favicons`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
messages.push({
|
|
48
|
+
status: types_1.CheckerStatus.Ok,
|
|
49
|
+
id: types_1.MessageId.icoFaviconDeclared,
|
|
50
|
+
text: 'The ICO favicon is declared'
|
|
51
|
+
});
|
|
52
|
+
const href = icos[0].attributes.href;
|
|
53
|
+
if (!href) {
|
|
54
|
+
messages.push({
|
|
55
|
+
status: types_1.CheckerStatus.Error,
|
|
56
|
+
id: types_1.MessageId.noIcoFaviconHref,
|
|
57
|
+
text: 'The ICO markup has no href attribute'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const iconUrl = (0, helper_1.mergeUrlAndPath)(url, href);
|
|
62
|
+
const iconResponse = yield fetcher(iconUrl, 'image/x-icon');
|
|
63
|
+
if (iconResponse.status === 404) {
|
|
64
|
+
messages.push({
|
|
65
|
+
status: types_1.CheckerStatus.Error,
|
|
66
|
+
id: types_1.MessageId.icoFavicon404,
|
|
67
|
+
text: `ICO favicon not found at ${iconUrl}`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (iconResponse.status >= 300 || !iconResponse.readableStream) {
|
|
71
|
+
messages.push({
|
|
72
|
+
status: types_1.CheckerStatus.Error,
|
|
73
|
+
id: types_1.MessageId.icoFaviconCannotGet,
|
|
74
|
+
text: `Error fetching ICO favicon at ${iconUrl} (status ${iconResponse.status})`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
messages.push({
|
|
79
|
+
status: types_1.CheckerStatus.Ok,
|
|
80
|
+
id: types_1.MessageId.icoFaviconDownloadable,
|
|
81
|
+
text: 'ICO favicon found'
|
|
82
|
+
});
|
|
83
|
+
const iconBuffer = yield (0, helper_1.readableStreamToBuffer)(iconResponse.readableStream);
|
|
84
|
+
const images = yield (0, decode_ico_1.default)(iconBuffer);
|
|
85
|
+
const imageSizes = images.map(image => `${image.width}x${image.height}`);
|
|
86
|
+
const expectedSizes = exports.IcoFaviconSizes.map(size => `${size}x${size}`);
|
|
87
|
+
const extraSizes = imageSizes.filter(size => !expectedSizes.includes(size));
|
|
88
|
+
if (extraSizes.length > 0) {
|
|
89
|
+
messages.push({
|
|
90
|
+
status: types_1.CheckerStatus.Warning,
|
|
91
|
+
id: types_1.MessageId.icoFaviconExtraSizes,
|
|
92
|
+
text: `Extra sizes found in ICO favicon: ${extraSizes.join(', ')}`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const missingSizes = expectedSizes.filter(size => !imageSizes.includes(size));
|
|
96
|
+
if (missingSizes.length > 0) {
|
|
97
|
+
messages.push({
|
|
98
|
+
status: types_1.CheckerStatus.Warning,
|
|
99
|
+
id: types_1.MessageId.icoFaviconMissingSizes,
|
|
100
|
+
text: `Missing sizes in ICO favicon: ${missingSizes.join(', ')}`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (extraSizes.length === 0 && missingSizes.length === 0) {
|
|
104
|
+
messages.push({
|
|
105
|
+
status: types_1.CheckerStatus.Ok,
|
|
106
|
+
id: types_1.MessageId.icoFaviconExpectedSizes,
|
|
107
|
+
text: `The ICO favicon has the expected sizes (${imageSizes.join(', ')})`
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return messages;
|
|
114
|
+
});
|
|
115
|
+
exports.checkIcoFavicon = checkIcoFavicon;
|
package/dist/helper.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Fetcher } from './types';
|
|
3
|
+
export declare const filePathToReadableStream: (path: string) => Promise<ReadableStream>;
|
|
4
|
+
export declare const filePathToString: (path: string) => Promise<string>;
|
|
5
|
+
export declare const stringToReadableStream: (str: string) => ReadableStream;
|
|
6
|
+
export declare const readableStreamToString: (readableStream: ReadableStream) => Promise<string>;
|
|
7
|
+
export declare const readableStreamToBuffer: (readableStream: ReadableStream) => Promise<Buffer>;
|
|
8
|
+
export type CheckIconProcessor = {
|
|
9
|
+
noHref: () => void;
|
|
10
|
+
icon404: () => void;
|
|
11
|
+
cannotGet: (httpStatusCode: number) => void;
|
|
12
|
+
downloadable: () => void;
|
|
13
|
+
square: (widthHeight: number) => void;
|
|
14
|
+
notSquare: (width: number, Height: number) => void;
|
|
15
|
+
rightSize: (widthHeight: number) => void;
|
|
16
|
+
wrongSize: (widthHeight: number) => void;
|
|
17
|
+
};
|
|
18
|
+
export declare const pathToMimeType: (path: string) => string;
|
|
19
|
+
export declare const checkIcon: (iconUrl: string | undefined, processor: CheckIconProcessor, fetcher: Fetcher, mimeType: string | undefined, expectedWidthHeight?: number) => Promise<string | null>;
|
|
20
|
+
export declare const mergeUrlAndPath: (baseUrl: string, absoluteOrRelativePath: string) => string;
|
|
21
|
+
export declare const parseSizesAttribute: (sizes: string | undefined | null) => number | null;
|
|
22
|
+
export declare const bufferToDataUrl: (buffer: Buffer, mimeType: string) => string;
|
|
23
|
+
export declare const fetchFetcher: Fetcher;
|