forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinstallable-manifest.js
267 lines (240 loc) · 14.4 KB
/
installable-manifest.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {Audit} from './audit.js';
import * as i18n from '../lib/i18n/i18n.js';
import {ManifestValues} from '../computed/manifest-values.js';
/* eslint-disable max-len */
const UIStrings = {
/** Title of a Lighthouse audit that provides detail on if a website is installable as an application. This descriptive title is shown to users when a webapp is installable. */
'title': 'Web app manifest and service worker meet the installability requirements',
/** Title of a Lighthouse audit that provides detail on if a website is installable as an application. This descriptive title is shown to users when a webapp is not installable. */
'failureTitle': 'Web app manifest or service worker do not meet the installability requirements',
/** Description of a Lighthouse audit that tells the user why installability is important for webapps. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
'description': `Service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. With proper service worker and manifest implementations, browsers can proactively prompt users to add your app to their homescreen, which can lead to higher engagement. [Learn more about manifest installability requirements](https://developer.chrome.com/docs/lighthouse/pwa/installable-manifest/).`,
/** Label for a column in a data table; entries in the column will be a string explaining why a failure occurred. */
'columnValue': 'Failure reason',
/**
* @description [ICU Syntax] Label for an audit identifying the number of installability errors found in the page.
*/
'displayValue': `{itemCount, plural,
=1 {1 reason}
other {# reasons}
}`,
/**
* @description Error message describing a DevTools error id that was found and has not been identified by this audit.
* @example {platform-not-supported-on-android} errorId
*/
'noErrorId': `Installability error id '{errorId}' is not recognized`,
/** Error message explaining that the page is not loaded in the frame. */
'not-in-main-frame': `Page is not loaded in the main frame`,
/** Error message explaining that the page is served from a secure origin. */
'not-from-secure-origin': 'Page is not served from a secure origin',
/** Error message explaining that the page has no manifest URL. */
'no-manifest': 'Page has no manifest <link> URL',
/** Error message explaining that the provided manifest URL is invalid. */
'start-url-not-valid': `Manifest start URL is not valid`,
/** Error message explaining that the provided manifest does not contain a name or short_name field. */
'manifest-missing-name-or-short-name': `Manifest does not contain a 'name' or 'short_name' field`,
/** Error message explaining that the manifest display property must be one of 'standalone', 'fullscreen', or 'minimal-ui'. */
'manifest-display-not-supported': `Manifest 'display' property must be one of 'standalone', 'fullscreen', or 'minimal-ui'`,
/** Error message explaining that the manifest could not be fetched, might be empty, or could not be parsed. */
'manifest-empty': `Manifest could not be fetched, is empty, or could not be parsed`,
// TODO: This error was removed in M114, we can remove this message when it hits stable.
/** Error message explaining that no matching service worker was detected,
* and provides a suggestion to reload the page or check whether the scope of the service worker
* for the current page encloses the scope and start URL from the manifest. */
'no-matching-service-worker': `No matching service worker detected. You may need to reload the page, or check that the scope of the service worker for the current page encloses the scope and start URL from the manifest.`,
/**
* @description Error message explaining that the manifest does not contain a suitable icon.
* @example {192} value0
*/
'manifest-missing-suitable-icon': `Manifest does not contain a suitable icon - PNG, SVG or WebP format of at least {value0}\xa0px is required, the sizes attribute must be set, and the purpose attribute, if set, must include "any".`,
/**
* @description Error message explaining that the manifest does not supply an icon of the correct format.
* @example {192} value0
*/
'no-acceptable-icon': `No supplied icon is at least {value0}\xa0px square in PNG, SVG or WebP format, with the purpose attribute unset or set to "any"`,
/** Error message explaining that the icon could not be downloaded. */
'cannot-download-icon': `Could not download a required icon from the manifest`,
/** Error message explaining that the downloaded icon was empty or corrupt. */
'no-icon-available': `Downloaded icon was empty or corrupted`,
/** Error message explaining that the specified application platform is not supported on Android. */
'platform-not-supported-on-android': `The specified application platform is not supported on Android`,
/** Error message explaining that a Play store ID was not provided. */
'no-id-specified': `No Play store ID provided`,
/** Error message explaining that the Play Store app URL and Play Store ID do not match. */
'ids-do-not-match': `The Play Store app URL and Play Store ID do not match`,
/** Error message explaining that the app is already installed. */
'already-installed': `The app is already installed`,
/** Error message explaining that a URL in the manifest contains a username, password, or port. */
'url-not-supported-for-webapk': `A URL in the manifest contains a username, password, or port`,
/** Error message explaining that the page is loaded in an incognito window. */
'in-incognito': `Page is loaded in an incognito window`,
// TODO: perhaps edit this message to make it more actionable for LH users
/** Error message explaining that the page does not work offline. */
'not-offline-capable': `Page does not work offline`,
/** Error message explaining that service worker could not be checked without a start_url. */
'no-url-for-service-worker': `Could not check service worker without a 'start_url' field in the manifest`,
/** Error message explaining that the manifest specifies prefer_related_applications: true. */
'prefer-related-applications': `Manifest specifies prefer_related_applications: true`,
/** Error message explaining that prefer_related_applications is only supported on Chrome Beta and Stable channels on Android. */
'prefer-related-applications-only-beta-stable': `prefer_related_applications is only supported on Chrome Beta and Stable channels on Android.`,
/** Error message explaining that the manifest contains 'display_override' field, and the
first supported display mode must be one of 'standalone', 'fullscreen', or 'minimal-ui'. */
'manifest-display-override-not-supported': `Manifest contains 'display_override' field, and the first supported display mode must be one of 'standalone', 'fullscreen', or 'minimal-ui'`,
/** Error message explaining that the web manifest's URL changed while the manifest was being downloaded by the browser. */
'manifest-location-changed': `Manifest URL changed while the manifest was being fetched.`,
/** Warning message explaining that the page does not work offline. */
'warn-not-offline-capable': `Page does not work offline. The page will not be regarded as installable after Chrome 93, stable release August 2021.`,
/** Error message explaining that Lighthouse failed while detecting a service worker, and directing the user to try again in a new Chrome. */
'protocol-timeout': `Lighthouse could not determine if there was a service worker. Please try with a newer version of Chrome.`,
/** Message logged when the web app has been uninstalled o desktop, signalling that the install banner state is being reset. */
'pipeline-restarted': 'PWA has been uninstalled and installability checks resetting.',
/**
* TODO: This error was removed in M114, we can remove this message when it hits stable.
* @description Error message explaining that the URL of the manifest uses a scheme that is not supported on Android.
* @example {data:} scheme
*/
'scheme-not-supported-for-webapk': 'The manifest URL scheme ({scheme}) is not supported on Android.',
};
/* eslint-enable max-len */
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
/**
* @fileoverview
* Audits if the page's web app manifest and service worker qualify for triggering a beforeinstallprompt event.
* https://github.com/GoogleChrome/lighthouse/issues/23#issuecomment-270453303
*
* Requirements based on Chrome Devtools' installability requirements.
* Origin of logging:
* https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/installable/installable_logging.cc
* DevTools InstallabilityError implementation:
* https://source.chromium.org/search?q=getInstallabilityErrorMessages&ss=chromium%2Fchromium%2Fsrc:third_party%2Fdevtools-frontend%2Fsrc%2Ffront_end%2Fresources%2F
*/
class InstallableManifest extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'installable-manifest',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
supportedModes: ['navigation'],
requiredArtifacts: ['WebAppManifest', 'InstallabilityErrors'],
};
}
/**
* @param {LH.Artifacts} artifacts
* @return {{i18nErrors: Array<LH.IcuMessage | string>; warnings: Array<LH.IcuMessage>}}
*/
static getInstallabilityErrors(artifacts) {
const installabilityErrors = artifacts.InstallabilityErrors.errors;
const i18nErrors = [];
const warnings = [];
const errorArgumentsRegex = /{([^}]+)}/g;
for (const err of installabilityErrors) {
// Filter out errorId 'in-incognito' since Lighthouse recommends incognito.
if (err.errorId === 'in-incognito') continue;
if (err.errorId === 'warn-not-offline-capable') {
warnings.push(str_(UIStrings[err.errorId]));
continue;
}
// Filter out errorId 'pipeline-restarted' since it only applies when the PWA is uninstalled.
if (err.errorId === 'pipeline-restarted') {
continue;
}
// @ts-expect-error errorIds from protocol should match up against the strings dict
const matchingString = UIStrings[err.errorId];
if (err.errorId === 'scheme-not-supported-for-webapk') {
// If there was no manifest, then there will be at lest one other installability error.
// We can ignore this error if that's the case.
const manifestUrl = artifacts.WebAppManifest?.url;
if (!manifestUrl) continue;
const scheme = new URL(manifestUrl).protocol;
i18nErrors.push(str_(matchingString, {scheme}));
continue;
}
// Handle an errorId we don't recognize.
if (matchingString === undefined) {
i18nErrors.push(str_(UIStrings.noErrorId, {errorId: err.errorId}));
continue;
}
// Get the i18m argument names of the installability error message, if any.
const UIStringArguments = matchingString.match(errorArgumentsRegex) || [];
/**
* If there is an argument value, get it.
* We only expect a `minimum-icon-size-in-pixels` errorArg[0] for two errorIds, currently.
*/
const value0 = err.errorArguments?.length && err.errorArguments[0].value;
if (matchingString && err.errorArguments.length !== UIStringArguments.length) {
// Matching string, but have the incorrect number of arguments for the message.
const stringArgs = JSON.stringify(err.errorArguments);
const msg = err.errorArguments.length > UIStringArguments.length ?
`${err.errorId} has unexpected arguments ${stringArgs}` :
`${err.errorId} does not have the expected number of arguments.`;
i18nErrors.push(msg);
} else if (matchingString && value0) {
i18nErrors.push(str_(matchingString, {value0}));
} else if (matchingString) {
i18nErrors.push(str_(matchingString));
}
}
return {i18nErrors, warnings};
}
/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*
*/
static async audit(artifacts, context) {
const {i18nErrors, warnings} = InstallableManifest.getInstallabilityErrors(artifacts);
const manifestUrl = artifacts.WebAppManifest ? artifacts.WebAppManifest.url : null;
/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
{key: 'reason', valueType: 'text', label: str_(UIStrings.columnValue)},
];
// Errors for report table.
/** @type {LH.Audit.Details.Table['items']} */
const errorReasons = i18nErrors.map(reason => {
return {reason};
});
// If InstallabilityErrors is empty, double check ManifestValues to make sure nothing was missed.
// InstallabilityErrors can be empty erroneously in our DevTools web tests.
if (!errorReasons.length) {
const manifestValues = await ManifestValues.request(artifacts, context);
if (manifestValues.isParseFailure) {
errorReasons.push({
reason: manifestValues.parseFailureReason,
});
}
}
// Include the detailed pass/fail checklist as a diagnostic.
/** @type {LH.Audit.Details.DebugData} */
const debugData = {
type: 'debugdata',
manifestUrl,
};
if (errorReasons.length > 0) {
return {
score: 0,
warnings,
numericValue: errorReasons.length,
numericUnit: 'element',
displayValue: str_(UIStrings.displayValue, {itemCount: errorReasons.length}),
details: {...Audit.makeTableDetails(headings, errorReasons), debugData},
};
}
return {
score: 1,
warnings,
details: {...Audit.makeTableDetails(headings, errorReasons), debugData},
};
}
}
export default InstallableManifest;
export {UIStrings};