/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
///
import * as assert from 'assert';
import {writeFile} from 'fs';
import * as path from 'path';
import * as logging from 'plylog';
import {generate as swPrecacheGenerate, SWConfig} from 'sw-precache';
import {DepsIndex} from './analyzer';
import {LocalFsPath, posixifyPath, PosixPath} from './path-transformers';
import {PolymerProject} from './polymer-project';
const logger = logging.getLogger('polymer-build.service-worker');
export interface AddServiceWorkerOptions {
project: PolymerProject;
buildRoot: LocalFsPath;
bundled?: boolean;
path?: LocalFsPath;
swPrecacheConfig?: SWConfig|null;
basePath?: LocalFsPath;
}
/**
* Given a user-provided AddServiceWorkerOptions object, check for deprecated
* options. When one is found, warn the user and fix if possible.
*/
// tslint:disable-next-line: no-any Turned off for user input.
function fixDeprecatedOptions(options: any): AddServiceWorkerOptions {
if (typeof options.serviceWorkerPath !== 'undefined') {
logger.warn(
'"serviceWorkerPath" config option has been renamed to "path" and will no longer be supported in future versions');
options.path = options.path || options.serviceWorkerPath;
}
if (typeof options.swConfig !== 'undefined') {
logger.warn(
'"swConfig" config option has been renamed to "swPrecacheConfig" and will no longer be supported in future versions');
options.swPrecacheConfig = options.swPrecacheConfig || options.swConfig;
}
return options;
}
/**
* Returns an array of file paths for the service worker to precache, based on
* the information provided in the DepsIndex object.
*/
function getPrecachedAssets(
depsIndex: DepsIndex, project: PolymerProject): string[] {
const precachedAssets = new Set(project.config.allFragments);
precachedAssets.add(project.config.entrypoint);
for (const depImports of depsIndex.fragmentToFullDeps.values()) {
depImports.imports.forEach((s) => precachedAssets.add(s));
depImports.scripts.forEach((s) => precachedAssets.add(s));
depImports.styles.forEach((s) => precachedAssets.add(s));
}
return Array.from(precachedAssets);
}
/**
* Returns an array of file paths for the service worker to precache for a
* BUNDLED build, based on the information provided in the DepsIndex object.
*/
function getBundledPrecachedAssets(project: PolymerProject) {
const precachedAssets = new Set(project.config.allFragments);
precachedAssets.add(project.config.entrypoint);
return Array.from(precachedAssets);
}
// Matches URLs like "/foo.png/bar" but not "/foo/bar.png".
export const hasNoFileExtension = /\/[^\/\.]*(\?|$)/;
/**
* Returns a promise that resolves with a generated service worker
* configuration.
*/
export async function generateServiceWorkerConfig(
options: AddServiceWorkerOptions): Promise {
assert(!!options, '`project` & `buildRoot` options are required');
assert(!!options.project, '`project` option is required');
assert(!!options.buildRoot, '`buildRoot` option is required');
options = fixDeprecatedOptions(options);
options = Object.assign({}, options);
const project = options.project;
const buildRoot = options.buildRoot;
const swPrecacheConfig: SWConfig =
Object.assign({}, options.swPrecacheConfig);
const depsIndex = await project.analyzer.analyzeDependencies;
let staticFileGlobs = Array.from(swPrecacheConfig.staticFileGlobs || []);
const precachedAssets = (options.bundled) ?
getBundledPrecachedAssets(project) :
getPrecachedAssets(depsIndex, project);
staticFileGlobs = staticFileGlobs.concat(precachedAssets);
staticFileGlobs = staticFileGlobs.map((filePath: string) => {
if (filePath.startsWith(project.config.root)) {
filePath = filePath.substring(project.config.root.length);
}
return path.join(buildRoot, filePath);
});
if (swPrecacheConfig.navigateFallback === undefined) {
// Map all application routes to the entrypoint.
swPrecacheConfig.navigateFallback =
path.relative(project.config.root, project.config.entrypoint);
}
if (swPrecacheConfig.navigateFallbackWhitelist === undefined) {
// Don't fall back to the entrypoint if the URL looks like a static file.
// We want those to 404 instead, since they are probably missing assets,
// not application routes. Note it's important that this matches the
// behavior of prpl-server.
swPrecacheConfig.navigateFallbackWhitelist = [hasNoFileExtension];
}
if (swPrecacheConfig.directoryIndex === undefined) {
// By default, sw-precache maps any path ending with "/" to "index.html".
// This is a reasonable default for matching application routes, but 1) our
// entrypoint might not be called "index.html", and 2) this case is already
// handled by the navigateFallback configuration above. Simplest to just
// disable this feature.
swPrecacheConfig.directoryIndex = '';
}
// swPrecache will determine the right urls by stripping buildRoot.
// NOTE:(usergenic) sw-precache generate() apparently replaces the
// prefix on an already posixified version of the path on win32.
//
// We include a trailing slash in `stripPrefix` so that we remove leading
// slashes on the pre-cache asset URLs, hence producing relative URLs
// instead of absolute. We want relative URLs for builds mounted at non-root
// paths. Note that service worker fetches are relative to its own URL.
swPrecacheConfig.stripPrefix = addTrailingSlash(posixifyPath(buildRoot));
if (options.basePath) {
// TODO Drop this feature once CLI doesn't depend on it.
let replacePrefix = posixifyPath(options.basePath);
if (!replacePrefix.endsWith('/')) {
replacePrefix = replacePrefix + '/' as PosixPath;
}
if (swPrecacheConfig.replacePrefix) {
console.info(
`Replacing service worker configuration's ` +
`replacePrefix option (${swPrecacheConfig.replacePrefix}) ` +
`with the build configuration's basePath (${replacePrefix}).`);
}
swPrecacheConfig.replacePrefix = replacePrefix;
}
// static files will be pre-cached
swPrecacheConfig.staticFileGlobs = staticFileGlobs;
// Log service-worker helpful output at the debug log level
swPrecacheConfig.logger = swPrecacheConfig.logger || logger.debug;
return swPrecacheConfig;
}
/**
* Returns a promise that resolves with a generated service worker (the file
* contents), based off of the options provided.
*/
export async function generateServiceWorker(options: AddServiceWorkerOptions):
Promise {
const swPrecacheConfig = await generateServiceWorkerConfig(options);
return await>(new Promise((resolve, reject) => {
logger.debug(`writing service worker...`, swPrecacheConfig);
swPrecacheGenerate(
swPrecacheConfig, (err?: Error, fileContents?: string) => {
if (err || fileContents == null) {
reject(err || 'No file contents provided.');
} else {
// Note: Node 10 Function.prototype.toString() produces output
// like `function() { }` where earlier versions produce
// `function () { }` (note the space between function keyword)
// and parentheses. To ensure the output is consistent across
// versions, we will correctively insert missing space here.
fileContents = fileContents.replace(/\bfunction\(/g, 'function (');
resolve(Buffer.from(fileContents));
}
});
}));
}
/**
* Returns a promise that resolves when a service worker has been generated
* and written to the build directory. This uses generateServiceWorker() to
* generate a service worker, which it then writes to the file system based on
* the buildRoot & path (if provided) options.
*/
export function addServiceWorker(options: AddServiceWorkerOptions):
Promise {
return generateServiceWorker(options).then((fileContents: Buffer) => {
return new Promise((resolve, reject) => {
const serviceWorkerPath =
path.join(options.buildRoot, options.path || 'service-worker.js');
writeFile(serviceWorkerPath, fileContents, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
});
}
function addTrailingSlash(s: string): string {
return s.endsWith('/') ? s : s + '/';
}