@realfavicongenerator/check-favicon 0.0.1
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 +0 -0
- package/fixtures/180x180.png +0 -0
- package/fixtures/192x192.png +0 -0
- package/fixtures/512x512.png +0 -0
- package/fixtures/happy-face.svg +1 -0
- package/fixtures/logo-transparent.png +0 -0
- package/fixtures/non-square.png +0 -0
- package/jest.config.js +5 -0
- package/package.json +38 -0
- package/src/desktop/desktop.test.ts +101 -0
- package/src/desktop/desktop.ts +207 -0
- package/src/desktop/ico.ts +110 -0
- package/src/helper.test.ts +128 -0
- package/src/helper.ts +170 -0
- package/src/index.ts +161 -0
- package/src/test-helper.ts +12 -0
- package/src/touch-icon.test.ts +219 -0
- package/src/touch-icon.ts +205 -0
- package/src/web-manifest.test.ts +196 -0
- package/src/web-manifest.ts +261 -0
- package/tsconfig.json +109 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { CheckerMessage, CheckerStatus, Fetcher, MessageId, TouchIconIconReport, TouchIconReport, TouchIconTitleReport, fetchFetcher } from ".";
|
|
2
|
+
import { HTMLElement } from 'node-html-parser'
|
|
3
|
+
import { CheckIconProcessor, checkIcon, mergeUrlAndPath } from "./helper";
|
|
4
|
+
|
|
5
|
+
export const checkTouchIconTitle = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<TouchIconTitleReport> => {
|
|
6
|
+
const messages: CheckerMessage[] = [];
|
|
7
|
+
let appTitle = undefined;
|
|
8
|
+
|
|
9
|
+
if (!head) {
|
|
10
|
+
messages.push({
|
|
11
|
+
status: CheckerStatus.Error,
|
|
12
|
+
id: MessageId.noHead,
|
|
13
|
+
text: 'No <head> element'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return { messages };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const titleMarkup = head.querySelectorAll("meta[name='apple-mobile-web-app-title']");
|
|
20
|
+
if (titleMarkup.length === 0) {
|
|
21
|
+
messages.push({
|
|
22
|
+
status: CheckerStatus.Warning,
|
|
23
|
+
id: MessageId.noTouchWebAppTitle,
|
|
24
|
+
text: 'No touch web app title declared'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return { messages };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (titleMarkup.length > 1) {
|
|
31
|
+
messages.push({
|
|
32
|
+
status: CheckerStatus.Error,
|
|
33
|
+
id: MessageId.multipleTouchWebAppTitles,
|
|
34
|
+
text: `The touch web app title is declared ${titleMarkup.length} times`
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return { messages };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!titleMarkup[0].getAttribute('content')) {
|
|
41
|
+
messages.push({
|
|
42
|
+
status: CheckerStatus.Error,
|
|
43
|
+
id: MessageId.emptyTouchWebAppTitle,
|
|
44
|
+
text: 'The touch web app title has no content'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { messages };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
appTitle = titleMarkup[0].getAttribute('content');
|
|
51
|
+
messages.push({
|
|
52
|
+
status: CheckerStatus.Ok,
|
|
53
|
+
id: MessageId.touchWebAppTitleDeclared,
|
|
54
|
+
text: `The touch web app title is "${appTitle}"`
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { messages, appTitle };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const checkTouchIconIcon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<TouchIconIconReport> => {
|
|
61
|
+
const messages: CheckerMessage[] = [];
|
|
62
|
+
let touchIcon: string | null = null;
|
|
63
|
+
|
|
64
|
+
if (!head) {
|
|
65
|
+
messages.push({
|
|
66
|
+
status: CheckerStatus.Error,
|
|
67
|
+
id: MessageId.noHead,
|
|
68
|
+
text: 'No <head> element'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return { messages, touchIcon };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const iconMarkup = head.querySelectorAll("link[rel='apple-touch-icon']");
|
|
75
|
+
if (iconMarkup.length === 0) {
|
|
76
|
+
messages.push({
|
|
77
|
+
status: CheckerStatus.Error,
|
|
78
|
+
id: MessageId.noTouchIcon,
|
|
79
|
+
text: 'No touch icon declared'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { messages, touchIcon };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
messages.push({
|
|
86
|
+
status: CheckerStatus.Ok,
|
|
87
|
+
id: MessageId.touchIconDeclared,
|
|
88
|
+
text: 'The touch icon is declared'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const sizes = iconMarkup.map(icon => icon.getAttribute('sizes')).filter(size => size);
|
|
92
|
+
if (sizes.length > 0) {
|
|
93
|
+
messages.push({
|
|
94
|
+
status: CheckerStatus.Warning,
|
|
95
|
+
id: MessageId.touchIconWithSize,
|
|
96
|
+
text: `Some Touch icon have a specific size (${sizes.join(', ')})`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const duplicatedSizes = getDuplicatedSizes(iconMarkup.map(icon => icon.getAttribute('sizes')));
|
|
101
|
+
if (duplicatedSizes.length > 0) {
|
|
102
|
+
messages.push({
|
|
103
|
+
status: CheckerStatus.Error,
|
|
104
|
+
id: MessageId.duplicatedTouchIconSizes,
|
|
105
|
+
text: `The touch icon sizes ${duplicatedSizes.map(s => s || '(no size)').join(', ')} are declared more than once`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const iconHref = iconMarkup[0].getAttribute('href');
|
|
110
|
+
if (!iconHref) {
|
|
111
|
+
messages.push({
|
|
112
|
+
status: CheckerStatus.Error,
|
|
113
|
+
id: MessageId.noTouchIconHref,
|
|
114
|
+
text: 'The touch icon has no href'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return { messages, touchIcon };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const touchIconUrl = mergeUrlAndPath(baseUrl, iconHref);
|
|
121
|
+
|
|
122
|
+
const processor: CheckIconProcessor = {
|
|
123
|
+
cannotGet: (status) => {
|
|
124
|
+
messages.push({
|
|
125
|
+
status: CheckerStatus.Error,
|
|
126
|
+
id: MessageId.touchIconCannotGet,
|
|
127
|
+
text: `The touch icon cannot be fetched (${status})`
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
downloadable: () => {
|
|
131
|
+
messages.push({
|
|
132
|
+
status: CheckerStatus.Ok,
|
|
133
|
+
id: MessageId.touchIconDownloadable,
|
|
134
|
+
text: 'The touch icon is downloadable'
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
icon404: () => {
|
|
138
|
+
messages.push({
|
|
139
|
+
status: CheckerStatus.Error,
|
|
140
|
+
id: MessageId.touchIcon404,
|
|
141
|
+
text: `The touch icon at ${touchIconUrl} is not found`
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
noHref: () => {
|
|
145
|
+
messages.push({
|
|
146
|
+
status: CheckerStatus.Error,
|
|
147
|
+
id: MessageId.noTouchIconHref,
|
|
148
|
+
text: 'The touch icon markup has no href'
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
notSquare: (width, height) => {
|
|
152
|
+
messages.push({
|
|
153
|
+
status: CheckerStatus.Error,
|
|
154
|
+
id: MessageId.touchIconNotSquare,
|
|
155
|
+
text: `The touch icon is not square (${width}x${height})`
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
rightSize: (width) => {
|
|
159
|
+
messages.push({
|
|
160
|
+
status: CheckerStatus.Ok,
|
|
161
|
+
id: MessageId.touchIconSquare,
|
|
162
|
+
text: `The touch icon is square (${width}x${width})`
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
square: (width) => {
|
|
166
|
+
messages.push({
|
|
167
|
+
status: CheckerStatus.Ok,
|
|
168
|
+
id: MessageId.touchIconSquare,
|
|
169
|
+
text: `The touch icon is square (${width}x${width})`
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
wrongSize: (width) => {
|
|
173
|
+
messages.push({
|
|
174
|
+
status: CheckerStatus.Error,
|
|
175
|
+
id: MessageId.touchIconWrongSize,
|
|
176
|
+
text: `The touch icon has a wrong size (${width}x${width})`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
touchIcon = await checkIcon(
|
|
182
|
+
touchIconUrl,
|
|
183
|
+
processor,
|
|
184
|
+
fetcher,
|
|
185
|
+
undefined
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return { messages, touchIcon };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const getDuplicatedSizes = (sizes: (string | undefined)[]): (string | undefined)[] => {
|
|
192
|
+
const duplicated = sizes.filter((size, index) => sizes.indexOf(size, index + 1) >= 0);
|
|
193
|
+
return duplicated.filter((size, index) => duplicated.indexOf(size) === index);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const checkTouchIcon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<TouchIconReport> => {
|
|
197
|
+
const titleReport = await checkTouchIconTitle(baseUrl, head, fetcher);
|
|
198
|
+
const iconReport = await checkTouchIconIcon(baseUrl, head, fetcher);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
messages: [...titleReport.messages, ...iconReport.messages],
|
|
202
|
+
appTitle: titleReport.appTitle,
|
|
203
|
+
touchIcon: iconReport.touchIcon
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import parse from "node-html-parser";
|
|
2
|
+
import { CheckerMessage, CheckerStatus, FetchResponse, MessageId, WebManifestReport } from ".";
|
|
3
|
+
import { checkWebManifest, checkWebManifestFile } from "./web-manifest";
|
|
4
|
+
import { testFetcher } from "./test-helper";
|
|
5
|
+
import { bufferToDataUrl, filePathToReadableStream, readableStreamToBuffer } from "./helper";
|
|
6
|
+
|
|
7
|
+
type TestOutput = {
|
|
8
|
+
messages: Pick<CheckerMessage, 'id' | 'status'>[],
|
|
9
|
+
name?: string,
|
|
10
|
+
shortName?: string,
|
|
11
|
+
backgroundColor?: string,
|
|
12
|
+
themeColor?: string,
|
|
13
|
+
icon?: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const filterOutput = (report: WebManifestReport): any => ({
|
|
17
|
+
...report,
|
|
18
|
+
messages: report.messages.map(m => ({ status: m.status, id: m.id }))
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const runCheckTouchIconTitleTest = async (
|
|
22
|
+
headFragment: string | null,
|
|
23
|
+
output: TestOutput,
|
|
24
|
+
fetchDatabase: { [url: string]: FetchResponse } = {}
|
|
25
|
+
) => {
|
|
26
|
+
const root = headFragment ? parse(headFragment) : null;
|
|
27
|
+
const result = await checkWebManifest('https://example.com/', root, testFetcher(fetchDatabase));
|
|
28
|
+
expect(filterOutput(result)).toEqual({
|
|
29
|
+
name: undefined,
|
|
30
|
+
shortName: undefined,
|
|
31
|
+
backgroundColor: undefined,
|
|
32
|
+
themeColor: undefined,
|
|
33
|
+
icon: null,
|
|
34
|
+
...output,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('checkWebManifest - noHead', async () => {
|
|
39
|
+
await runCheckTouchIconTitleTest(null, { messages: [{
|
|
40
|
+
status: CheckerStatus.Error,
|
|
41
|
+
id: MessageId.noHead,
|
|
42
|
+
}]});
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('checkWebManifest - noManifest', async () => {
|
|
46
|
+
await runCheckTouchIconTitleTest('<title>Hey</title>', { messages: [{
|
|
47
|
+
status: CheckerStatus.Error,
|
|
48
|
+
id: MessageId.noManifest,
|
|
49
|
+
}]});
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('checkWebManifest - noManifestHref', async () => {
|
|
53
|
+
await runCheckTouchIconTitleTest('<link rel="manifest" />', { messages: [{
|
|
54
|
+
status: CheckerStatus.Error,
|
|
55
|
+
id: MessageId.noManifestHref,
|
|
56
|
+
}]});
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('checkWebManifest - manifest404', async () => {
|
|
60
|
+
await runCheckTouchIconTitleTest('<link rel="manifest" href="not-found.json" />', { messages: [{
|
|
61
|
+
status: CheckerStatus.Error,
|
|
62
|
+
id: MessageId.manifest404,
|
|
63
|
+
}]});
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('checkWebManifest - manifestCannotGet', async () => {
|
|
67
|
+
await runCheckTouchIconTitleTest('<link rel="manifest" href="/error.json" />', { messages: [{
|
|
68
|
+
status: CheckerStatus.Error,
|
|
69
|
+
id: MessageId.manifestCannotGet,
|
|
70
|
+
}]}, {
|
|
71
|
+
'https://example.com/error.json': {
|
|
72
|
+
status: 500,
|
|
73
|
+
contentType: 'application/json',
|
|
74
|
+
readableStream: null
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('checkWebManifest - manifestInvalidJson', async () => {
|
|
80
|
+
await runCheckTouchIconTitleTest('<link rel="manifest" href="/bad-manifest.json" />', { messages: [{
|
|
81
|
+
status: CheckerStatus.Error,
|
|
82
|
+
id: MessageId.manifestInvalidJson,
|
|
83
|
+
}]}, {
|
|
84
|
+
'https://example.com/bad-manifest.json': {
|
|
85
|
+
status: 200,
|
|
86
|
+
contentType: 'application/json',
|
|
87
|
+
readableStream: stringToReadableStream('{ bad JSON }')
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const stringToReadableStream = (str: string) => {
|
|
93
|
+
const stream = new ReadableStream({
|
|
94
|
+
start(controller) {
|
|
95
|
+
controller.enqueue(new TextEncoder().encode(str));
|
|
96
|
+
controller.close();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return stream;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test('checkWebManifestFile - Missing fields', async () => {
|
|
103
|
+
const report = await checkWebManifestFile({
|
|
104
|
+
name: null,
|
|
105
|
+
short_name: null,
|
|
106
|
+
background_color: null,
|
|
107
|
+
theme_color: null,
|
|
108
|
+
icons: []
|
|
109
|
+
}, 'https://example.com/', testFetcher({}));
|
|
110
|
+
|
|
111
|
+
expect(filterOutput(report)).toEqual({
|
|
112
|
+
messages: [{
|
|
113
|
+
status: CheckerStatus.Error,
|
|
114
|
+
id: MessageId.noManifestName,
|
|
115
|
+
}, {
|
|
116
|
+
status: CheckerStatus.Error,
|
|
117
|
+
id: MessageId.noManifestShortName,
|
|
118
|
+
}, {
|
|
119
|
+
status: CheckerStatus.Error,
|
|
120
|
+
id: MessageId.noManifestBackgroundColor,
|
|
121
|
+
}, {
|
|
122
|
+
status: CheckerStatus.Error,
|
|
123
|
+
id: MessageId.noManifestThemeColor,
|
|
124
|
+
}, {
|
|
125
|
+
status: CheckerStatus.Error,
|
|
126
|
+
id: MessageId.noManifestIcons,
|
|
127
|
+
}],
|
|
128
|
+
name: undefined,
|
|
129
|
+
shortName: undefined,
|
|
130
|
+
backgroundColor: undefined,
|
|
131
|
+
themeColor: undefined,
|
|
132
|
+
icon: null
|
|
133
|
+
});
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const testIcon192 = './fixtures/192x192.png';
|
|
137
|
+
const testIcon512 = './fixtures/512x512.png';
|
|
138
|
+
|
|
139
|
+
test('checkWebManifestFile - Everything is fine', async () => {
|
|
140
|
+
const report = await checkWebManifestFile({
|
|
141
|
+
name: 'My long name',
|
|
142
|
+
short_name: 'Short!',
|
|
143
|
+
background_color: '#123456',
|
|
144
|
+
theme_color: '#abcdef',
|
|
145
|
+
icons: [
|
|
146
|
+
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
|
147
|
+
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
|
|
148
|
+
]
|
|
149
|
+
}, 'https://example.com/', testFetcher({
|
|
150
|
+
'https://example.com/icon-192.png': {
|
|
151
|
+
status: 200,
|
|
152
|
+
contentType: 'image/png',
|
|
153
|
+
readableStream: await filePathToReadableStream(testIcon192)
|
|
154
|
+
},
|
|
155
|
+
'https://example.com/icon-512.png': {
|
|
156
|
+
status: 200,
|
|
157
|
+
contentType: 'image/png',
|
|
158
|
+
readableStream: await filePathToReadableStream(testIcon512)
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const expectedIconReport = [
|
|
163
|
+
{
|
|
164
|
+
status: CheckerStatus.Ok,
|
|
165
|
+
id: MessageId.manifestIconDeclared,
|
|
166
|
+
}, {
|
|
167
|
+
status: CheckerStatus.Ok,
|
|
168
|
+
id: MessageId.manifestIconDownloadable,
|
|
169
|
+
}, {
|
|
170
|
+
status: CheckerStatus.Ok,
|
|
171
|
+
id: MessageId.manifestIconRightSize,
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
expect(filterOutput(report)).toEqual({
|
|
176
|
+
messages: [{
|
|
177
|
+
status: CheckerStatus.Ok,
|
|
178
|
+
id: MessageId.manifestName,
|
|
179
|
+
}, {
|
|
180
|
+
status: CheckerStatus.Ok,
|
|
181
|
+
id: MessageId.manifestShortName,
|
|
182
|
+
}, {
|
|
183
|
+
status: CheckerStatus.Ok,
|
|
184
|
+
id: MessageId.manifestBackgroundColor,
|
|
185
|
+
}, {
|
|
186
|
+
status: CheckerStatus.Ok,
|
|
187
|
+
id: MessageId.manifestThemeColor,
|
|
188
|
+
},
|
|
189
|
+
...expectedIconReport, ...expectedIconReport], // Two icons
|
|
190
|
+
name: 'My long name',
|
|
191
|
+
shortName: 'Short!',
|
|
192
|
+
backgroundColor: '#123456',
|
|
193
|
+
themeColor: '#abcdef',
|
|
194
|
+
icon: bufferToDataUrl(await readableStreamToBuffer(await filePathToReadableStream(testIcon512)), 'image/png')
|
|
195
|
+
});
|
|
196
|
+
})
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { HTMLElement } from "node-html-parser";
|
|
2
|
+
import { CheckerMessage, CheckerStatus, Fetcher, MessageId, WebManifestReport, fetchFetcher, pathToMimeType } from ".";
|
|
3
|
+
import { CheckIconProcessor, checkIcon, mergeUrlAndPath } from "./helper";
|
|
4
|
+
|
|
5
|
+
export const checkWebManifest = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<WebManifestReport> => {
|
|
6
|
+
const messages: CheckerMessage[] = [];
|
|
7
|
+
let name = undefined;
|
|
8
|
+
let shortName = undefined;
|
|
9
|
+
let backgroundColor = undefined;
|
|
10
|
+
let themeColor = undefined;
|
|
11
|
+
let icon = null;
|
|
12
|
+
|
|
13
|
+
if (!head) {
|
|
14
|
+
messages.push({
|
|
15
|
+
status: CheckerStatus.Error,
|
|
16
|
+
id: MessageId.noHead,
|
|
17
|
+
text: 'No <head> element'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const manifestMarkup = head.querySelectorAll("link[rel='manifest']");
|
|
24
|
+
if (manifestMarkup.length === 0) {
|
|
25
|
+
messages.push({
|
|
26
|
+
status: CheckerStatus.Error,
|
|
27
|
+
id: MessageId.noManifest,
|
|
28
|
+
text: 'No web app manifest'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const href = manifestMarkup[0].getAttribute('href');
|
|
35
|
+
if (!href) {
|
|
36
|
+
messages.push({
|
|
37
|
+
status: CheckerStatus.Error,
|
|
38
|
+
id: MessageId.noManifestHref,
|
|
39
|
+
text: 'The web app manifest markup has no `href` attribute'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const manifestUrl = mergeUrlAndPath(baseUrl, href);
|
|
46
|
+
|
|
47
|
+
const manifest = await fetcher(manifestUrl, 'application/json');
|
|
48
|
+
|
|
49
|
+
if (manifest.status === 404) {
|
|
50
|
+
messages.push({
|
|
51
|
+
status: CheckerStatus.Error,
|
|
52
|
+
id: MessageId.manifest404,
|
|
53
|
+
text: `The web app manifest at \`${href}\` is not found`
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
57
|
+
} else if (manifest.status >= 300 || !manifest.readableStream) {
|
|
58
|
+
messages.push({
|
|
59
|
+
status: CheckerStatus.Error,
|
|
60
|
+
id: MessageId.manifestCannotGet,
|
|
61
|
+
text: `Cannot get the web app manifest at \`${href}\` (${manifest.status} error)`
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let parsedManifest;
|
|
68
|
+
try {
|
|
69
|
+
parsedManifest = await readableStreamToJson(manifest.readableStream);
|
|
70
|
+
} catch(e) {
|
|
71
|
+
messages.push({
|
|
72
|
+
status: CheckerStatus.Error,
|
|
73
|
+
id: MessageId.manifestInvalidJson,
|
|
74
|
+
text: `Cannot parse the web app manifest at \`${href}\``
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const manifestReport = await checkWebManifestFile(parsedManifest, manifestUrl, fetcher);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...manifestReport,
|
|
84
|
+
messages: messages.concat(manifestReport.messages),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const readableStreamToJson = async (stream: ReadableStream): Promise<any> => {
|
|
89
|
+
const reader = stream.getReader();
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
let result = '';
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
result += decoder.decode(value);
|
|
98
|
+
}
|
|
99
|
+
return JSON.parse(result);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const checkWebManifestFile = async (manifest: any, baseUrl: string, fetcher: Fetcher): Promise<WebManifestReport> => {
|
|
103
|
+
const messages: CheckerMessage[] = [];
|
|
104
|
+
let icon = null;
|
|
105
|
+
|
|
106
|
+
const name = manifest.name || undefined;
|
|
107
|
+
if (!name) {
|
|
108
|
+
messages.push({
|
|
109
|
+
status: CheckerStatus.Error,
|
|
110
|
+
id: MessageId.noManifestName,
|
|
111
|
+
text: 'The web app manifest has no `name`'
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
messages.push({
|
|
115
|
+
status: CheckerStatus.Ok,
|
|
116
|
+
id: MessageId.manifestName,
|
|
117
|
+
text: `The web app manifest has the name "${name}"`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const shortName = manifest.short_name || undefined;
|
|
122
|
+
if (!shortName) {
|
|
123
|
+
messages.push({
|
|
124
|
+
status: CheckerStatus.Error,
|
|
125
|
+
id: MessageId.noManifestShortName,
|
|
126
|
+
text: 'The web app manifest has no `short_name`'
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
messages.push({
|
|
130
|
+
status: CheckerStatus.Ok,
|
|
131
|
+
id: MessageId.manifestShortName,
|
|
132
|
+
text: `The web app manifest has the short name "${shortName}"`
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const backgroundColor = manifest.background_color || undefined;
|
|
137
|
+
if (!backgroundColor) {
|
|
138
|
+
messages.push({
|
|
139
|
+
status: CheckerStatus.Error,
|
|
140
|
+
id: MessageId.noManifestBackgroundColor,
|
|
141
|
+
text: 'The web app manifest has no `background_color`'
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
messages.push({
|
|
145
|
+
status: CheckerStatus.Ok,
|
|
146
|
+
id: MessageId.manifestBackgroundColor,
|
|
147
|
+
text: `The web app manifest has the background color \`${backgroundColor}\``
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const themeColor = manifest.theme_color || undefined;
|
|
152
|
+
if (!themeColor) {
|
|
153
|
+
messages.push({
|
|
154
|
+
status: CheckerStatus.Error,
|
|
155
|
+
id: MessageId.noManifestThemeColor,
|
|
156
|
+
text: 'The web app manifest has no `theme_color`'
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
messages.push({
|
|
160
|
+
status: CheckerStatus.Ok,
|
|
161
|
+
id: MessageId.manifestThemeColor,
|
|
162
|
+
text: `The web app manifest has the theme color \`${themeColor}\``
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const icons = manifest.icons;
|
|
167
|
+
|
|
168
|
+
if (!icons || !Array.isArray(icons) || icons.length === 0) {
|
|
169
|
+
messages.push({
|
|
170
|
+
status: CheckerStatus.Error,
|
|
171
|
+
id: MessageId.noManifestIcons,
|
|
172
|
+
text: 'The web app manifest has no `icons`'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for await (const size of [ 192, 512 ]) {
|
|
179
|
+
const iconEntry = icons.find((icon: any) => icon.sizes === `${size}x${size}`);
|
|
180
|
+
if (!iconEntry) {
|
|
181
|
+
messages.push({
|
|
182
|
+
status: CheckerStatus.Error,
|
|
183
|
+
id: MessageId.noManifestIcon,
|
|
184
|
+
text: `The web app manifest has no ${size}x${size} icon`
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
messages.push({
|
|
188
|
+
status: CheckerStatus.Ok,
|
|
189
|
+
id: MessageId.manifestIconDeclared,
|
|
190
|
+
text: `The web app manifest has a ${size}x${size} icon`
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const iconUrl = mergeUrlAndPath(baseUrl, iconEntry.src);
|
|
194
|
+
|
|
195
|
+
const processor: CheckIconProcessor = {
|
|
196
|
+
cannotGet: (status) => {
|
|
197
|
+
messages.push({
|
|
198
|
+
status: CheckerStatus.Error,
|
|
199
|
+
id: MessageId.manifestIconCannotGet,
|
|
200
|
+
text: `The ${size}x${size} icon cannot be fetched (${status})`
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
downloadable: () => {
|
|
204
|
+
messages.push({
|
|
205
|
+
status: CheckerStatus.Ok,
|
|
206
|
+
id: MessageId.manifestIconDownloadable,
|
|
207
|
+
text: `The ${size}x${size} icon is downloadable`
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
icon404: () => {
|
|
211
|
+
messages.push({
|
|
212
|
+
status: CheckerStatus.Error,
|
|
213
|
+
id: MessageId.manifestIcon404,
|
|
214
|
+
text: `The ${size}x${size} icon is not found`
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
noHref: () => {
|
|
218
|
+
messages.push({
|
|
219
|
+
status: CheckerStatus.Error,
|
|
220
|
+
id: MessageId.manifestIconNoHref,
|
|
221
|
+
text: `The ${size}x${size} icon has no \`href\` attribute`
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
notSquare: () => {
|
|
225
|
+
messages.push({
|
|
226
|
+
status: CheckerStatus.Error,
|
|
227
|
+
id: MessageId.manifestIconNotSquare,
|
|
228
|
+
text: `The ${size}x${size} icon is not square`
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
rightSize: () => {
|
|
232
|
+
messages.push({
|
|
233
|
+
status: CheckerStatus.Ok,
|
|
234
|
+
id: MessageId.manifestIconRightSize,
|
|
235
|
+
text: `The ${size}x${size} icon has the right size`
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
square: () => {
|
|
239
|
+
// Ignore this, just check the size
|
|
240
|
+
},
|
|
241
|
+
wrongSize: (actualSize) => {
|
|
242
|
+
messages.push({
|
|
243
|
+
status: CheckerStatus.Error,
|
|
244
|
+
id: MessageId.manifestIconWrongSize,
|
|
245
|
+
text: `The ${size}x${size} icon has the wrong size (${actualSize})`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
icon = await checkIcon(
|
|
251
|
+
iconUrl,
|
|
252
|
+
processor,
|
|
253
|
+
fetcher,
|
|
254
|
+
iconEntry.type || pathToMimeType(iconEntry.src),
|
|
255
|
+
size
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { messages, name, shortName, backgroundColor, themeColor, icon };
|
|
261
|
+
}
|