forked from GoogleChrome/lighthouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathservice-worker.js
179 lines (160 loc) · 7.32 KB
/
service-worker.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
/**
* @license Copyright 2016 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.
*/
'use strict';
const URL = require('../lib/url-shim.js');
const Audit = require('./audit.js');
const i18n = require('../lib/i18n/i18n.js');
const UIStrings = {
/** Title of a Lighthouse audit that provides detail on a page's service worker. This descriptive title is shown to users when a service worker is registered and valid. */
title: 'Registers a service worker that controls page and `start_url`',
/** Title of a Lighthouse audit that provides detail on a page's service worker. This descriptive title is shown to users when a service worker is not present or invalid. */
failureTitle: 'Does not register a service worker that controls page and `start_url`',
/** Description of a Lighthouse audit that tells the user why they should use a service worker. This is displayed after a user expands the section to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
description: 'The 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. [Learn more](https://web.dev/service-worker/).',
/**
* @description Message explaining that the website may have service workers, but none are in scope to control the tested web page.
* @example {https://example.com/} pageUrl
* */
explanationOutOfScope: 'This origin has one or more service workers, however the page ' +
'({pageUrl}) is not in scope.',
/** Message explaining that the page has no manifest file so can't specify a starting url. */
explanationNoManifest: 'This page is controlled by a service worker, however ' +
'no `start_url` was found because no manifest was fetched.',
/** Message explaining that the page had an invalid manifest file so can't specify a starting url. */
explanationBadManifest: 'This page is controlled by a service worker, however ' +
'no `start_url` was found because manifest failed to parse as valid JSON',
/**
* @description Message explaining that the website has a service worker, but none are in scope to control the tested starting url.
* @example {https://example.com/} startUrl
* @example {https://othersite.com/} scopeUrl
* */
explanationBadStartUrl: 'This page is controlled by a service worker, however ' +
'the `start_url` ({startUrl}) is not in the service worker\'s scope ({scopeUrl})',
};
const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
class ServiceWorker extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'service-worker',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['URL', 'ServiceWorker', 'WebAppManifest'],
};
}
/**
* Find active service workers for this origin.
* @param {Array<LH.Crdp.ServiceWorker.ServiceWorkerVersion>} versions
* @param {URL} pageUrl
* @return {Array<LH.Crdp.ServiceWorker.ServiceWorkerVersion>}
*/
static getVersionsForOrigin(versions, pageUrl) {
return versions
.filter(v => v.status === 'activated')
.filter(v => new URL(v.scriptURL).origin === pageUrl.origin);
}
/**
* From the set of active service workers for this origin, find the controlling SW (if any)
* and return its scope URL.
* @param {Array<LH.Crdp.ServiceWorker.ServiceWorkerVersion>} matchingSWVersions
* @param {Array<LH.Crdp.ServiceWorker.ServiceWorkerRegistration>} registrations
* @param {URL} pageUrl
* @return {{scopeUrl: string; scriptUrl: string} | undefined}
*/
static getControllingServiceWorker(matchingSWVersions, registrations, pageUrl) {
// Find the normalized scope URLs of possibly-controlling SWs.
/** @type {Array<{scopeUrl: string; scriptUrl: string}>} */
const scriptByScopeUrlList = [];
// Populate serviceWorkerUrls map with the scriptURLs and scopeUrls of matchingSWVersions and registrations
for (const version of matchingSWVersions) {
const matchedRegistration = registrations
.find(r => r.registrationId === version.registrationId);
if (matchedRegistration) {
const scopeUrl = new URL(matchedRegistration.scopeURL).href;
const scriptUrl = new URL(version.scriptURL).href;
scriptByScopeUrlList.push({scopeUrl, scriptUrl});
}
}
// Find most-specific applicable scope, the one controlling the page.
// See https://w3c.github.io/ServiceWorker/v1/#scope-match-algorithm
const pageControllingUrls = scriptByScopeUrlList
.filter(ss => pageUrl.href.startsWith(ss.scopeUrl))
.sort((ssA, ssB) => ssA.scopeUrl.length - ssB.scopeUrl.length)
.pop();
return pageControllingUrls;
}
/**
* Returns a failure message if there is no start_url or if the start_url isn't
* contolled by the scopeUrl.
* @param {LH.Artifacts['WebAppManifest']} WebAppManifest
* @param {string} scopeUrl
* @return {LH.IcuMessage|undefined}
*/
static checkStartUrl(WebAppManifest, scopeUrl) {
if (!WebAppManifest) {
return str_(UIStrings.explanationNoManifest);
}
if (!WebAppManifest.value) {
return str_(UIStrings.explanationBadManifest);
}
const startUrl = WebAppManifest.value.start_url.value;
if (!startUrl.startsWith(scopeUrl)) {
return str_(UIStrings.explanationBadStartUrl, {startUrl, scopeUrl});
}
}
/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.Product}
*/
static audit(artifacts) {
// Match against artifacts.URL.finalUrl so audit accounts for any redirects.
const pageUrl = new URL(artifacts.URL.finalUrl);
const {versions, registrations} = artifacts.ServiceWorker;
const versionsForOrigin = ServiceWorker.getVersionsForOrigin(versions, pageUrl);
if (versionsForOrigin.length === 0) {
return {
score: 0,
};
}
const serviceWorkerUrls = ServiceWorker.getControllingServiceWorker(versionsForOrigin,
registrations, pageUrl);
if (!serviceWorkerUrls) {
return {
score: 0,
explanation: str_(UIStrings.explanationOutOfScope, {pageUrl: pageUrl.href}),
};
}
// Include the SW details as diagnostic data.
const {scriptUrl, scopeUrl} = serviceWorkerUrls;
/** @type {LH.Audit.Details.DebugData} */
const details = {
type: 'debugdata',
scriptUrl,
scopeUrl,
};
const startUrlFailure = ServiceWorker.checkStartUrl(artifacts.WebAppManifest,
serviceWorkerUrls.scopeUrl);
if (startUrlFailure) {
return {
score: 0,
details,
explanation: startUrlFailure,
};
}
// SW controls both finalUrl and start_url.
return {
score: 1,
details,
};
}
}
module.exports = ServiceWorker;
module.exports.UIStrings = UIStrings;