UNPKG

9.01 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
4 * This code may only be used under the BSD style license found at
5 * http://polymer.github.io/LICENSE.txt
6 * The complete set of authors may be found at
7 * http://polymer.github.io/AUTHORS.txt
8 * The complete set of contributors may be found at
9 * http://polymer.github.io/CONTRIBUTORS.txt
10 * Code distributed by Google as part of the polymer project is also
11 * subject to an additional IP rights grant found at
12 * http://polymer.github.io/PATENTS.txt
13 */
14
15/// <reference path="../custom_typings/sw-precache.d.ts" />
16
17import * as assert from 'assert';
18import {writeFile} from 'fs';
19import * as path from 'path';
20import * as logging from 'plylog';
21import {generate as swPrecacheGenerate, SWConfig} from 'sw-precache';
22
23import {DepsIndex} from './analyzer';
24import {LocalFsPath, posixifyPath, PosixPath} from './path-transformers';
25import {PolymerProject} from './polymer-project';
26
27const logger = logging.getLogger('polymer-build.service-worker');
28
29export interface AddServiceWorkerOptions {
30 project: PolymerProject;
31 buildRoot: LocalFsPath;
32 bundled?: boolean;
33 path?: LocalFsPath;
34 swPrecacheConfig?: SWConfig|null;
35 basePath?: LocalFsPath;
36}
37
38/**
39 * Given a user-provided AddServiceWorkerOptions object, check for deprecated
40 * options. When one is found, warn the user and fix if possible.
41 */
42// tslint:disable-next-line: no-any Turned off for user input.
43function fixDeprecatedOptions(options: any): AddServiceWorkerOptions {
44 if (typeof options.serviceWorkerPath !== 'undefined') {
45 logger.warn(
46 '"serviceWorkerPath" config option has been renamed to "path" and will no longer be supported in future versions');
47 options.path = options.path || options.serviceWorkerPath;
48 }
49 if (typeof options.swConfig !== 'undefined') {
50 logger.warn(
51 '"swConfig" config option has been renamed to "swPrecacheConfig" and will no longer be supported in future versions');
52 options.swPrecacheConfig = options.swPrecacheConfig || options.swConfig;
53 }
54 return options;
55}
56
57/**
58 * Returns an array of file paths for the service worker to precache, based on
59 * the information provided in the DepsIndex object.
60 */
61function getPrecachedAssets(
62 depsIndex: DepsIndex, project: PolymerProject): string[] {
63 const precachedAssets = new Set<string>(project.config.allFragments);
64 precachedAssets.add(project.config.entrypoint);
65
66 for (const depImports of depsIndex.fragmentToFullDeps.values()) {
67 depImports.imports.forEach((s) => precachedAssets.add(s));
68 depImports.scripts.forEach((s) => precachedAssets.add(s));
69 depImports.styles.forEach((s) => precachedAssets.add(s));
70 }
71
72 return Array.from(precachedAssets);
73}
74
75/**
76 * Returns an array of file paths for the service worker to precache for a
77 * BUNDLED build, based on the information provided in the DepsIndex object.
78 */
79function getBundledPrecachedAssets(project: PolymerProject) {
80 const precachedAssets = new Set<string>(project.config.allFragments);
81 precachedAssets.add(project.config.entrypoint);
82
83 return Array.from(precachedAssets);
84}
85
86// Matches URLs like "/foo.png/bar" but not "/foo/bar.png".
87export const hasNoFileExtension = /\/[^\/\.]*(\?|$)/;
88
89/**
90 * Returns a promise that resolves with a generated service worker
91 * configuration.
92 */
93export async function generateServiceWorkerConfig(
94 options: AddServiceWorkerOptions): Promise<SWConfig> {
95 assert(!!options, '`project` & `buildRoot` options are required');
96 assert(!!options.project, '`project` option is required');
97 assert(!!options.buildRoot, '`buildRoot` option is required');
98 options = fixDeprecatedOptions(options);
99
100 options = Object.assign({}, options);
101 const project = options.project;
102 const buildRoot = options.buildRoot;
103 const swPrecacheConfig: SWConfig =
104 Object.assign({}, options.swPrecacheConfig);
105
106 const depsIndex = await project.analyzer.analyzeDependencies;
107 let staticFileGlobs = Array.from(swPrecacheConfig.staticFileGlobs || []);
108 const precachedAssets = (options.bundled) ?
109 getBundledPrecachedAssets(project) :
110 getPrecachedAssets(depsIndex, project);
111
112 staticFileGlobs = staticFileGlobs.concat(precachedAssets);
113 staticFileGlobs = staticFileGlobs.map((filePath: string) => {
114 if (filePath.startsWith(project.config.root)) {
115 filePath = filePath.substring(project.config.root.length);
116 }
117 return path.join(buildRoot, filePath);
118 });
119
120 if (swPrecacheConfig.navigateFallback === undefined) {
121 // Map all application routes to the entrypoint.
122 swPrecacheConfig.navigateFallback =
123 path.relative(project.config.root, project.config.entrypoint);
124 }
125
126 if (swPrecacheConfig.navigateFallbackWhitelist === undefined) {
127 // Don't fall back to the entrypoint if the URL looks like a static file.
128 // We want those to 404 instead, since they are probably missing assets,
129 // not application routes. Note it's important that this matches the
130 // behavior of prpl-server.
131 swPrecacheConfig.navigateFallbackWhitelist = [hasNoFileExtension];
132 }
133
134 if (swPrecacheConfig.directoryIndex === undefined) {
135 // By default, sw-precache maps any path ending with "/" to "index.html".
136 // This is a reasonable default for matching application routes, but 1) our
137 // entrypoint might not be called "index.html", and 2) this case is already
138 // handled by the navigateFallback configuration above. Simplest to just
139 // disable this feature.
140 swPrecacheConfig.directoryIndex = '';
141 }
142
143 // swPrecache will determine the right urls by stripping buildRoot.
144 // NOTE:(usergenic) sw-precache generate() apparently replaces the
145 // prefix on an already posixified version of the path on win32.
146 //
147 // We include a trailing slash in `stripPrefix` so that we remove leading
148 // slashes on the pre-cache asset URLs, hence producing relative URLs
149 // instead of absolute. We want relative URLs for builds mounted at non-root
150 // paths. Note that service worker fetches are relative to its own URL.
151 swPrecacheConfig.stripPrefix = addTrailingSlash(posixifyPath(buildRoot));
152
153 if (options.basePath) {
154 // TODO Drop this feature once CLI doesn't depend on it.
155 let replacePrefix = posixifyPath(options.basePath);
156 if (!replacePrefix.endsWith('/')) {
157 replacePrefix = replacePrefix + '/' as PosixPath;
158 }
159 if (swPrecacheConfig.replacePrefix) {
160 console.info(
161 `Replacing service worker configuration's ` +
162 `replacePrefix option (${swPrecacheConfig.replacePrefix}) ` +
163 `with the build configuration's basePath (${replacePrefix}).`);
164 }
165 swPrecacheConfig.replacePrefix = replacePrefix;
166 }
167
168 // static files will be pre-cached
169 swPrecacheConfig.staticFileGlobs = staticFileGlobs;
170
171 // Log service-worker helpful output at the debug log level
172 swPrecacheConfig.logger = swPrecacheConfig.logger || logger.debug;
173
174 return swPrecacheConfig;
175}
176
177/**
178 * Returns a promise that resolves with a generated service worker (the file
179 * contents), based off of the options provided.
180 */
181export async function generateServiceWorker(options: AddServiceWorkerOptions):
182 Promise<Buffer> {
183 const swPrecacheConfig = await generateServiceWorkerConfig(options);
184 return await<Promise<Buffer>>(new Promise((resolve, reject) => {
185 logger.debug(`writing service worker...`, swPrecacheConfig);
186 swPrecacheGenerate(
187 swPrecacheConfig, (err?: Error, fileContents?: string) => {
188 if (err || fileContents == null) {
189 reject(err || 'No file contents provided.');
190 } else {
191 // Note: Node 10 Function.prototype.toString() produces output
192 // like `function() { }` where earlier versions produce
193 // `function () { }` (note the space between function keyword)
194 // and parentheses. To ensure the output is consistent across
195 // versions, we will correctively insert missing space here.
196 fileContents = fileContents.replace(/\bfunction\(/g, 'function (');
197 resolve(Buffer.from(fileContents));
198 }
199 });
200 }));
201}
202
203/**
204 * Returns a promise that resolves when a service worker has been generated
205 * and written to the build directory. This uses generateServiceWorker() to
206 * generate a service worker, which it then writes to the file system based on
207 * the buildRoot & path (if provided) options.
208 */
209export function addServiceWorker(options: AddServiceWorkerOptions):
210 Promise<void> {
211 return generateServiceWorker(options).then((fileContents: Buffer) => {
212 return new Promise<void>((resolve, reject) => {
213 const serviceWorkerPath =
214 path.join(options.buildRoot, options.path || 'service-worker.js');
215 writeFile(serviceWorkerPath, fileContents, (err) => {
216 if (err) {
217 reject(err);
218 } else {
219 resolve();
220 }
221 });
222 });
223 });
224}
225
226function addTrailingSlash(s: string): string {
227 return s.endsWith('/') ? s : s + '/';
228}
229
\No newline at end of file