PWA
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m24s

This commit is contained in:
eewing
2026-02-19 07:41:49 -06:00
parent 5776a1fdb1
commit c0fae761da
10919 changed files with 874329 additions and 13511 deletions
@@ -0,0 +1,58 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {errors} from './errors';
import {ManifestEntry} from '../types';
type AdditionalManifestEntriesTransform = {
(manifest: Array<ManifestEntry & {size: number}>): {
manifest: Array<ManifestEntry & {size: number}>;
warnings: string[];
};
};
export function additionalManifestEntriesTransform(
additionalManifestEntries: Array<ManifestEntry | string>,
): AdditionalManifestEntriesTransform {
return (manifest: Array<ManifestEntry & {size: number}>) => {
const warnings: Array<string> = [];
const stringEntries = new Set<string>();
for (const additionalEntry of additionalManifestEntries) {
// Warn about either a string or an object that lacks a revision property.
// (An object with a revision property set to null is okay.)
if (typeof additionalEntry === 'string') {
stringEntries.add(additionalEntry);
manifest.push({
revision: null,
size: 0,
url: additionalEntry,
});
} else {
if (additionalEntry && additionalEntry.revision === undefined) {
stringEntries.add(additionalEntry.url);
}
manifest.push(Object.assign({size: 0}, additionalEntry));
}
}
if (stringEntries.size > 0) {
let urls = '\n';
for (const stringEntry of stringEntries) {
urls += ` - ${stringEntry}\n`;
}
warnings.push(errors['string-entry-warning'] + urls);
}
return {
manifest,
warnings,
};
};
}
+152
View File
@@ -0,0 +1,152 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {babel} from '@rollup/plugin-babel';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import {rollup, Plugin} from 'rollup';
import terser from '@rollup/plugin-terser';
import {writeFile} from 'fs-extra';
import omt from '@surma/rollup-plugin-off-main-thread';
import presetEnv from '@babel/preset-env';
import replace from '@rollup/plugin-replace';
import tempy from 'tempy';
import upath from 'upath';
import {GeneratePartial, RequiredSWDestPartial} from '../types';
interface NameAndContents {
contents: string | Uint8Array;
name: string;
}
export async function bundle({
babelPresetEnvTargets,
inlineWorkboxRuntime,
mode,
sourcemap,
swDest,
unbundledCode,
}: Omit<GeneratePartial, 'runtimeCaching'> &
RequiredSWDestPartial & {unbundledCode: string}): Promise<
Array<NameAndContents>
> {
// We need to write this to the "real" file system, as Rollup won't read from
// a custom file system.
const {dir, base} = upath.parse(swDest);
const temporaryFile = tempy.file({name: base});
await writeFile(temporaryFile, unbundledCode);
const plugins = [
nodeResolve(),
replace({
// See https://github.com/GoogleChrome/workbox/issues/2769
'preventAssignment': true,
'process.env.NODE_ENV': JSON.stringify(mode),
}),
babel({
babelHelpers: 'bundled',
// Disable the logic that checks for local Babel config files:
// https://github.com/GoogleChrome/workbox/issues/2111
babelrc: false,
configFile: false,
presets: [
[
presetEnv,
{
targets: {
browsers: babelPresetEnvTargets,
},
loose: true,
},
],
],
}),
];
if (mode === 'production') {
plugins.push(
terser({
mangle: {
toplevel: true,
properties: {
regex: /(^_|_$)/,
},
},
}),
);
}
const rollupConfig: {
input: string;
manualChunks?: (id: string) => string | undefined;
plugins: Array<Plugin>;
} = {
plugins,
input: temporaryFile,
};
// Rollup will inline the runtime by default. If we don't want that, we need
// to add in some additional config.
if (!inlineWorkboxRuntime) {
// No lint for omt(), library has no types.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
rollupConfig.plugins.unshift(omt());
rollupConfig.manualChunks = (id) => {
return id.includes('workbox') ? 'workbox' : undefined;
};
}
const bundle = await rollup(rollupConfig);
const {output} = await bundle.generate({
sourcemap,
// Using an external Workbox runtime requires 'amd'.
format: inlineWorkboxRuntime ? 'es' : 'amd',
});
const files: Array<NameAndContents> = [];
for (const chunkOrAsset of output) {
if (chunkOrAsset.type === 'asset') {
files.push({
name: chunkOrAsset.fileName,
contents: chunkOrAsset.source,
});
} else {
let code = chunkOrAsset.code;
if (chunkOrAsset.map) {
const sourceMapFile = chunkOrAsset.fileName + '.map';
code += `//# sourceMappingURL=${sourceMapFile}\n`;
files.push({
name: sourceMapFile,
contents: chunkOrAsset.map.toString(),
});
}
files.push({
name: chunkOrAsset.fileName,
contents: code,
});
}
}
// Make sure that if there was a directory portion included in swDest, it's
// preprended to all of the generated files.
return files.map((file) => {
file.name = upath.format({
dir,
base: file.name,
ext: '',
name: '',
root: '',
});
return file;
});
}
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright 2021 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {ok} from 'assert';
import {BuildType, WorkboxPackageJSON} from '../types';
import {errors} from './errors';
import * as cdn from '../cdn-details.json';
function getVersionedURL(): string {
return `${getCDNPrefix()}/${cdn.latestVersion}`;
}
function getCDNPrefix() {
return `${cdn.origin}/${cdn.bucketName}/${cdn.releasesDir}`;
}
export function getModuleURL(moduleName: string, buildType: BuildType): string {
ok(moduleName, errors['no-module-name']);
if (buildType) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const pkgJson: WorkboxPackageJSON = require(`${moduleName}/package.json`);
if (buildType === 'dev' && pkgJson.workbox && pkgJson.workbox.prodOnly) {
// This is not due to a public-facing exception, so just throw an Error(),
// without creating an entry in errors.js.
throw Error(`The 'dev' build of ${moduleName} is not available.`);
}
return `${getVersionedURL()}/${moduleName}.${buildType.slice(0, 4)}.js`;
}
return `${getVersionedURL()}/${moduleName}.js`;
}
+84
View File
@@ -0,0 +1,84 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import fse from 'fs-extra';
import upath from 'upath';
import {WorkboxPackageJSON} from '../types';
import {errors} from './errors';
// Used to filter the libraries to copy based on our package.json dependencies.
const WORKBOX_PREFIX = 'workbox-';
// The directory within each package containing the final bundles.
const BUILD_DIR = 'build';
/**
* This copies over a set of runtime libraries used by Workbox into a
* local directory, which should be deployed alongside your service worker file.
*
* As an alternative to deploying these local copies, you could instead use
* Workbox from its official CDN URL.
*
* This method is exposed for the benefit of developers using
* {@link workbox-build.injectManifest} who would
* prefer not to use the CDN copies of Workbox. Developers using
* {@link workbox-build.generateSW} don't need to
* explicitly call this method.
*
* @param {string} destDirectory The path to the parent directory under which
* the new directory of libraries will be created.
* @return {Promise<string>} The name of the newly created directory.
*
* @alias workbox-build.copyWorkboxLibraries
*/
export async function copyWorkboxLibraries(
destDirectory: string,
): Promise<string> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const thisPkg: WorkboxPackageJSON = require('../../package.json');
// Use the version string from workbox-build in the name of the parent
// directory. This should be safe, because lerna will bump workbox-build's
// pkg.version whenever one of the dependent libraries gets bumped, and we
// care about versioning the dependent libraries.
const workboxDirectoryName = `workbox-v${
thisPkg.version ? thisPkg.version : ''
}`;
const workboxDirectoryPath = upath.join(destDirectory, workboxDirectoryName);
await fse.ensureDir(workboxDirectoryPath);
const copyPromises: Array<Promise<void>> = [];
const librariesToCopy = Object.keys(thisPkg.dependencies || {}).filter(
(dependency) => dependency.startsWith(WORKBOX_PREFIX),
);
for (const library of librariesToCopy) {
// Get the path to the package on the user's filesystem by require-ing
// the package's `package.json` file via the node resolution algorithm.
const libraryPath = upath.dirname(
require.resolve(`${library}/package.json`),
);
const buildPath = upath.join(libraryPath, BUILD_DIR);
// fse.copy() copies all the files in a directory, not the directory itself.
// See https://github.com/jprichardson/node-fs-extra/blob/master/docs/copy.md#copysrc-dest-options-callback
copyPromises.push(fse.copy(buildPath, workboxDirectoryPath));
}
try {
await Promise.all(copyPromises);
return workboxDirectoryName;
} catch (error) {
throw Error(
`${errors['unable-to-copy-workbox-libraries']} ${
error instanceof Error ? error.toString() : ''
}`,
);
}
}
+124
View File
@@ -0,0 +1,124 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {oneLine as ol} from 'common-tags';
export const errors = {
'unable-to-get-rootdir': `Unable to get the root directory of your web app.`,
'no-extension': ol`Unable to detect a usable extension for a file in your web
app directory.`,
'invalid-file-manifest-name': ol`The File Manifest Name must have at least one
character.`,
'unable-to-get-file-manifest-name': 'Unable to get a file manifest name.',
'invalid-sw-dest': `The 'swDest' value must be a valid path.`,
'unable-to-get-sw-name': 'Unable to get a service worker file name.',
'unable-to-get-save-config': ol`An error occurred when asking to save details
in a config file.`,
'unable-to-get-file-hash': ol`An error occurred when attempting to create a
file hash.`,
'unable-to-get-file-size': ol`An error occurred when attempting to get a file
size.`,
'unable-to-glob-files': 'An error occurred when globbing for files.',
'unable-to-make-manifest-directory': ol`Unable to make output directory for
file manifest.`,
'read-manifest-template-failure': 'Unable to read template for file manifest',
'populating-manifest-tmpl-failed': ol`An error occurred when populating the
file manifest template.`,
'manifest-file-write-failure': 'Unable to write the file manifest.',
'unable-to-make-sw-directory': ol`Unable to make the directories to output
the service worker path.`,
'read-sw-template-failure': ol`Unable to read the service worker template
file.`,
'sw-write-failure': 'Unable to write the service worker file.',
'sw-write-failure-directory': ol`Unable to write the service worker file;
'swDest' should be a full path to the file, not a path to a directory.`,
'unable-to-copy-workbox-libraries': ol`One or more of the Workbox libraries
could not be copied over to the destination directory: `,
'invalid-generate-sw-input': ol`The input to generateSW() must be an object.`,
'invalid-glob-directory': ol`The supplied globDirectory must be a path as a
string.`,
'invalid-dont-cache-bust': ol`The supplied 'dontCacheBustURLsMatching'
parameter must be a RegExp.`,
'invalid-exclude-files': 'The excluded files should be an array of strings.',
'invalid-get-manifest-entries-input': ol`The input to
'getFileManifestEntries()' must be an object.`,
'invalid-manifest-path': ol`The supplied manifest path is not a string with
at least one character.`,
'invalid-manifest-entries': ol`The manifest entries must be an array of
strings or JavaScript objects containing a url parameter.`,
'invalid-manifest-format': ol`The value of the 'format' option passed to
generateFileManifest() must be either 'iife' (the default) or 'es'.`,
'invalid-static-file-globs': ol`The 'globPatterns' value must be an array
of strings.`,
'invalid-templated-urls': ol`The 'templatedURLs' value should be an object
that maps URLs to either a string, or to an array of glob patterns.`,
'templated-url-matches-glob': ol`One of the 'templatedURLs' URLs is already
being tracked via 'globPatterns': `,
'invalid-glob-ignores': ol`The 'globIgnores' parameter must be an array of
glob pattern strings.`,
'manifest-entry-bad-url': ol`The generated manifest contains an entry without
a URL string. This is likely an error with workbox-build.`,
'modify-url-prefix-bad-prefixes': ol`The 'modifyURLPrefix' parameter must be
an object with string key value pairs.`,
'invalid-inject-manifest-arg': ol`The input to 'injectManifest()' must be an
object.`,
'injection-point-not-found': ol`Unable to find a place to inject the manifest.
Please ensure that your service worker file contains the following: `,
'multiple-injection-points': ol`Please ensure that your 'swSrc' file contains
only one match for the following: `,
'populating-sw-tmpl-failed': ol`Unable to generate service worker from
template.`,
'useless-glob-pattern': ol`One of the glob patterns doesn't match any files.
Please remove or fix the following: `,
'bad-template-urls-asset': ol`There was an issue using one of the provided
'templatedURLs'.`,
'invalid-runtime-caching': ol`The 'runtimeCaching' parameter must an an
array of objects with at least a 'urlPattern' and 'handler'.`,
'static-file-globs-deprecated': ol`'staticFileGlobs' is deprecated.
Please use 'globPatterns' instead.`,
'dynamic-url-deprecated': ol`'dynamicURLToDependencies' is deprecated.
Please use 'templatedURLs' instead.`,
'urlPattern-is-required': ol`The 'urlPattern' option is required when using
'runtimeCaching'.`,
'handler-is-required': ol`The 'handler' option is required when using
runtimeCaching.`,
'invalid-generate-file-manifest-arg': ol`The input to generateFileManifest()
must be an Object.`,
'invalid-sw-src': `The 'swSrc' file can't be read.`,
'same-src-and-dest': ol`Unable to find a place to inject the manifest. This is
likely because swSrc and swDest are configured to the same file.
Please ensure that your swSrc file contains the following:`,
'only-regexp-routes-supported': ol`Please use a regular expression object as
the urlPattern parameter. (Express-style routes are not currently
supported.)`,
'bad-runtime-caching-config': ol`An unknown configuration option was used
with runtimeCaching: `,
'invalid-network-timeout-seconds': ol`When using networkTimeoutSeconds, you
must set the handler to 'NetworkFirst'.`,
'no-module-name': ol`You must provide a moduleName parameter when calling
getModuleURL().`,
'bad-manifest-transforms-return-value': ol`The return value from a
manifestTransform should be an object with 'manifest' and optionally
'warnings' properties.`,
'string-entry-warning': ol`Some items were passed to additionalManifestEntries
without revisioning info. This is generally NOT safe. Learn more at
https://bit.ly/wb-precache.`,
'no-manifest-entries-or-runtime-caching': ol`Couldn't find configuration for
either precaching or runtime caching. Please ensure that the various glob
options are set to match one or more files, and/or configure the
runtimeCaching option.`,
'cant-find-sourcemap': ol`The swSrc file refers to a sourcemap that can't be
opened:`,
'nav-preload-runtime-caching': ol`When using navigationPreload, you must also
configure a runtimeCaching route that will use the preloaded response.`,
'cache-name-required': ol`When using cache expiration, you must also
configure a custom cacheName.`,
'manifest-transforms': ol`When using manifestTransforms, you must provide
an array of functions.`,
'invalid-handler-string': ol`The handler name provided is not valid: `,
};
+12
View File
@@ -0,0 +1,12 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
export function escapeRegExp(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
+34
View File
@@ -0,0 +1,34 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import crypto from 'crypto';
import {FileDetails} from '../types';
export function getCompositeDetails(
compositeURL: string,
dependencyDetails: Array<FileDetails>,
): FileDetails {
let totalSize = 0;
let compositeHash = '';
for (const fileDetails of dependencyDetails) {
totalSize += fileDetails.size;
compositeHash += fileDetails.hash;
}
const md5 = crypto.createHash('md5');
md5.update(compositeHash);
const hashOfHashes = md5.digest('hex');
return {
file: compositeURL,
hash: hashOfHashes,
size: totalSize,
};
}
+75
View File
@@ -0,0 +1,75 @@
/*
Copyright 2021 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {globSync} from 'glob';
import upath from 'upath';
import {errors} from './errors';
import {getFileSize} from './get-file-size';
import {getFileHash} from './get-file-hash';
import {GlobPartial} from '../types';
interface FileDetails {
file: string;
hash: string;
size: number;
}
export function getFileDetails({
globDirectory,
globFollow,
globIgnores,
globPattern,
}: Omit<GlobPartial, 'globDirectory' | 'globPatterns' | 'templatedURLs'> & {
// This will only be called when globDirectory is not undefined.
globDirectory: string;
globPattern: string;
}): {
globbedFileDetails: Array<FileDetails>;
warning: string;
} {
let globbedFiles: Array<string>;
let warning = '';
try {
globbedFiles = globSync(globPattern, {
cwd: globDirectory,
follow: globFollow,
ignore: globIgnores,
});
} catch (err) {
throw new Error(
errors['unable-to-glob-files'] +
` '${err instanceof Error && err.message ? err.message : ''}'`,
);
}
if (globbedFiles.length === 0) {
warning =
errors['useless-glob-pattern'] +
' ' +
JSON.stringify({globDirectory, globPattern, globIgnores}, null, 2);
}
const globbedFileDetails: Array<FileDetails> = [];
for (const file of globbedFiles) {
const fullPath = upath.join(globDirectory, file);
const fileSize = getFileSize(fullPath);
if (fileSize !== null) {
const fileHash = getFileHash(fullPath);
globbedFileDetails.push({
file: `${upath.relative(globDirectory, fullPath)}`,
hash: fileHash,
size: fileSize,
});
}
}
return {globbedFileDetails, warning};
}
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import fse from 'fs-extra';
import {getStringHash} from './get-string-hash';
import {errors} from './errors';
export function getFileHash(file: string): string {
try {
const buffer = fse.readFileSync(file);
return getStringHash(buffer);
} catch (err) {
throw new Error(
errors['unable-to-get-file-hash'] +
` '${err instanceof Error && err.message ? err.message : ''}'`,
);
}
}
+118
View File
@@ -0,0 +1,118 @@
/*
Copyright 2021 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import assert from 'assert';
import {GetManifestResult, FileDetails, GetManifestOptions} from '../types';
import {errors} from './errors';
import {getCompositeDetails} from './get-composite-details';
import {getFileDetails} from './get-file-details';
import {getStringDetails} from './get-string-details';
import {transformManifest} from './transform-manifest';
export async function getFileManifestEntries({
additionalManifestEntries,
dontCacheBustURLsMatching,
globDirectory,
globFollow,
globIgnores,
globPatterns = [],
manifestTransforms,
maximumFileSizeToCacheInBytes,
modifyURLPrefix,
templatedURLs,
}: GetManifestOptions): Promise<GetManifestResult> {
const warnings: Array<string> = [];
const allFileDetails = new Map<string, FileDetails>();
try {
for (const globPattern of globPatterns) {
const {globbedFileDetails, warning} = getFileDetails({
globDirectory,
globFollow,
globIgnores,
globPattern,
});
if (warning) {
warnings.push(warning);
}
for (const details of globbedFileDetails) {
if (details && !allFileDetails.has(details.file)) {
allFileDetails.set(details.file, details);
}
}
}
} catch (error) {
// If there's an exception thrown while globbing, then report
// it back as a warning, and don't consider it fatal.
if (error instanceof Error && error.message) {
warnings.push(error.message);
}
}
if (templatedURLs) {
for (const url of Object.keys(templatedURLs)) {
assert(!allFileDetails.has(url), errors['templated-url-matches-glob']);
const dependencies = templatedURLs[url];
if (Array.isArray(dependencies)) {
const details = dependencies.reduce<Array<FileDetails>>(
(previous, globPattern) => {
try {
const {globbedFileDetails, warning} = getFileDetails({
globDirectory,
globFollow,
globIgnores,
globPattern,
});
if (warning) {
warnings.push(warning);
}
return previous.concat(globbedFileDetails);
} catch (error) {
const debugObj: {[key: string]: Array<string>} = {};
debugObj[url] = dependencies;
throw new Error(
`${errors['bad-template-urls-asset']} ` +
`'${globPattern}' from '${JSON.stringify(debugObj)}':\n` +
`${error instanceof Error ? error.toString() : ''}`,
);
}
},
[],
);
if (details.length === 0) {
throw new Error(
`${errors['bad-template-urls-asset']} The glob ` +
`pattern '${dependencies.toString()}' did not match anything.`,
);
}
allFileDetails.set(url, getCompositeDetails(url, details));
} else if (typeof dependencies === 'string') {
allFileDetails.set(url, getStringDetails(url, dependencies));
}
}
}
const transformedManifest = await transformManifest({
additionalManifestEntries,
dontCacheBustURLsMatching,
manifestTransforms,
maximumFileSizeToCacheInBytes,
modifyURLPrefix,
fileDetails: Array.from(allFileDetails.values()),
});
transformedManifest.warnings.push(...warnings);
return transformedManifest;
}
+26
View File
@@ -0,0 +1,26 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import fse from 'fs-extra';
import {errors} from './errors';
export function getFileSize(file: string): number | null {
try {
const stat = fse.statSync(file);
if (!stat.isFile()) {
return null;
}
return stat.size;
} catch (err) {
throw new Error(
errors['unable-to-get-file-size'] +
` '${err instanceof Error && err.message ? err.message : ''}'`,
);
}
}
+32
View File
@@ -0,0 +1,32 @@
/*
Copyright 2022 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
// Adapted from https://github.com/lydell/source-map-url/blob/master/source-map-url.js
// See https://github.com/GoogleChrome/workbox/issues/3019
const innerRegex = /[#@] sourceMappingURL=([^\s'"]*)/;
const regex = RegExp(
'(?:' +
'/\\*' +
'(?:\\s*\r?\n(?://)?)?' +
'(?:' +
innerRegex.source +
')' +
'\\s*' +
'\\*/' +
'|' +
'//(?:' +
innerRegex.source +
')' +
')' +
'\\s*',
);
export function getSourceMapURL(srcContents: string): string | null {
const match = srcContents.match(regex);
return match ? match[1] || match[2] || '' : null;
}
+18
View File
@@ -0,0 +1,18 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {FileDetails} from '../types';
import {getStringHash} from './get-string-hash';
export function getStringDetails(url: string, str: string): FileDetails {
return {
file: url,
hash: getStringHash(str),
size: str.length,
};
}
+15
View File
@@ -0,0 +1,15 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import crypto from 'crypto';
export function getStringHash(input: crypto.BinaryLike): string {
const md5 = crypto.createHash('md5');
md5.update(input);
return md5.digest('hex');
}
+33
View File
@@ -0,0 +1,33 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import prettyBytes from 'pretty-bytes';
import {ManifestTransform} from '../types';
export function maximumSizeTransform(
maximumFileSizeToCacheInBytes: number,
): ManifestTransform {
return (originalManifest) => {
const warnings: Array<string> = [];
const manifest = originalManifest.filter((entry) => {
if (entry.size <= maximumFileSizeToCacheInBytes) {
return true;
}
warnings.push(
`${entry.url} is ${prettyBytes(entry.size)}, and won't ` +
`be precached. Configure maximumFileSizeToCacheInBytes to change ` +
`this limit.`,
);
return false;
});
return {manifest, warnings};
};
}
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {errors} from './errors';
import {escapeRegExp} from './escape-regexp';
import {ManifestTransform} from '../types';
export function modifyURLPrefixTransform(modifyURLPrefix: {
[key: string]: string;
}): ManifestTransform {
if (
!modifyURLPrefix ||
typeof modifyURLPrefix !== 'object' ||
Array.isArray(modifyURLPrefix)
) {
throw new Error(errors['modify-url-prefix-bad-prefixes']);
}
// If there are no entries in modifyURLPrefix, just return an identity
// function as a shortcut.
if (Object.keys(modifyURLPrefix).length === 0) {
return (manifest) => {
return {manifest};
};
}
for (const key of Object.keys(modifyURLPrefix)) {
if (typeof modifyURLPrefix[key] !== 'string') {
throw new Error(errors['modify-url-prefix-bad-prefixes']);
}
}
// Escape the user input so it's safe to use in a regex.
const safeModifyURLPrefixes = Object.keys(modifyURLPrefix).map(escapeRegExp);
// Join all the `modifyURLPrefix` keys so a single regex can be used.
const prefixMatchesStrings = safeModifyURLPrefixes.join('|');
// Add `^` to the front the prefix matches so it only matches the start of
// a string.
const modifyRegex = new RegExp(`^(${prefixMatchesStrings})`);
return (originalManifest) => {
const manifest = originalManifest.map((entry) => {
if (typeof entry.url !== 'string') {
throw new Error(errors['manifest-entry-bad-url']);
}
entry.url = entry.url.replace(modifyRegex, (match) => {
return modifyURLPrefix[match];
});
return entry;
});
return {manifest};
};
}
+73
View File
@@ -0,0 +1,73 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {oneLine as ol} from 'common-tags';
import upath from 'upath';
/**
* Class for keeping track of which Workbox modules are used by the generated
* service worker script.
*
* @private
*/
export class ModuleRegistry {
private readonly _modulesUsed: Map<string, {moduleName: string; pkg: string}>;
/**
* @private
*/
constructor() {
this._modulesUsed = new Map();
}
/**
* @return {Array<string>} A list of all of the import statements that are
* needed for the modules being used.
* @private
*/
getImportStatements(): Array<string> {
const workboxModuleImports: Array<string> = [];
for (const [localName, {moduleName, pkg}] of this._modulesUsed) {
// By default require.resolve returns the resolved path of the 'main'
// field, which might be deeper than the package root. To work around
// this, we can find the package's root by resolving its package.json and
// strip the '/package.json' from the resolved path.
const pkgJsonPath = require.resolve(`${pkg}/package.json`);
const pkgRoot = upath.dirname(pkgJsonPath);
const importStatement = ol`import {${moduleName} as ${localName}} from
'${pkgRoot}/${moduleName}.mjs';`;
workboxModuleImports.push(importStatement);
}
return workboxModuleImports;
}
/**
* @param {string} pkg The workbox package that the module belongs to.
* @param {string} moduleName The name of the module to import.
* @return {string} The local variable name that corresponds to that module.
* @private
*/
getLocalName(pkg: string, moduleName: string): string {
return `${pkg.replace(/-/g, '_')}_${moduleName}`;
}
/**
* @param {string} pkg The workbox package that the module belongs to.
* @param {string} moduleName The name of the module to import.
* @return {string} The local variable name that corresponds to that module.
* @private
*/
use(pkg: string, moduleName: string): string {
const localName = this.getLocalName(pkg, moduleName);
this._modulesUsed.set(localName, {moduleName, pkg});
return localName;
}
}
@@ -0,0 +1,34 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {errors} from './errors';
import {ManifestTransform} from '../types';
export function noRevisionForURLsMatchingTransform(
regexp: RegExp,
): ManifestTransform {
if (!(regexp instanceof RegExp)) {
throw new Error(errors['invalid-dont-cache-bust']);
}
return (originalManifest) => {
const manifest = originalManifest.map((entry) => {
if (typeof entry.url !== 'string') {
throw new Error(errors['manifest-entry-bad-url']);
}
if (entry.url.match(regexp)) {
entry.revision = null;
}
return entry;
});
return {manifest};
};
}
+105
View File
@@ -0,0 +1,105 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import template from 'lodash/template';
import {errors} from './errors';
import {GeneratePartial, ManifestEntry} from '../types';
import {ModuleRegistry} from './module-registry';
import {runtimeCachingConverter} from './runtime-caching-converter';
import {stringifyWithoutComments} from './stringify-without-comments';
import {swTemplate} from '../templates/sw-template';
export function populateSWTemplate({
cacheId,
cleanupOutdatedCaches,
clientsClaim,
directoryIndex,
disableDevLogs,
ignoreURLParametersMatching,
importScripts,
manifestEntries = [],
navigateFallback,
navigateFallbackDenylist,
navigateFallbackAllowlist,
navigationPreload,
offlineGoogleAnalytics,
runtimeCaching = [],
skipWaiting,
}: GeneratePartial & {manifestEntries?: Array<ManifestEntry>}): string {
// There needs to be at least something to precache, or else runtime caching.
if (!(manifestEntries?.length > 0 || runtimeCaching.length > 0)) {
throw new Error(errors['no-manifest-entries-or-runtime-caching']);
}
// These are all options that can be passed to the precacheAndRoute() method.
const precacheOptions = {
directoryIndex,
// An array of RegExp objects can't be serialized by JSON.stringify()'s
// default behavior, so if it's given, convert it manually.
ignoreURLParametersMatching: ignoreURLParametersMatching
? ([] as Array<RegExp>)
: undefined,
};
let precacheOptionsString = JSON.stringify(precacheOptions, null, 2);
if (ignoreURLParametersMatching) {
precacheOptionsString = precacheOptionsString.replace(
`"ignoreURLParametersMatching": []`,
`"ignoreURLParametersMatching": [` +
`${ignoreURLParametersMatching.join(', ')}]`,
);
}
let offlineAnalyticsConfigString: string | undefined = undefined;
if (offlineGoogleAnalytics) {
// If offlineGoogleAnalytics is a truthy value, we need to convert it to the
// format expected by the template.
offlineAnalyticsConfigString =
offlineGoogleAnalytics === true
? // If it's the literal value true, then use an empty config string.
'{}'
: // Otherwise, convert the config object into a more complex string, taking
// into account the fact that functions might need to be stringified.
stringifyWithoutComments(offlineGoogleAnalytics);
}
const moduleRegistry = new ModuleRegistry();
try {
const populatedTemplate = template(swTemplate)({
cacheId,
cleanupOutdatedCaches,
clientsClaim,
disableDevLogs,
importScripts,
manifestEntries,
navigateFallback,
navigateFallbackDenylist,
navigateFallbackAllowlist,
navigationPreload,
offlineAnalyticsConfigString,
precacheOptionsString,
runtimeCaching: runtimeCachingConverter(moduleRegistry, runtimeCaching),
skipWaiting,
use: moduleRegistry.use.bind(moduleRegistry),
});
const workboxImportStatements = moduleRegistry.getImportStatements();
// We need the import statements for all of the Workbox runtime modules
// prepended, so that the correct bundle can be created.
return workboxImportStatements.join('\n') + populatedTemplate;
} catch (error) {
throw new Error(
`${errors['populating-sw-tmpl-failed']} '${
error instanceof Error && error.message ? error.message : ''
}'`,
);
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import upath from 'upath';
export function rebasePath({
baseDirectory,
file,
}: {
baseDirectory: string;
file: string;
}): string {
// The initial path is relative to the current directory, so make it absolute.
const absolutePath = upath.resolve(file);
// Convert the absolute path so that it's relative to the baseDirectory.
const relativePath = upath.relative(baseDirectory, absolutePath);
// Remove any leading ./ as it won't work in a glob pattern.
const normalizedPath = upath.normalize(relativePath);
return normalizedPath;
}
+122
View File
@@ -0,0 +1,122 @@
/*
Copyright 2019 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map';
/**
* Adapted from https://github.com/nsams/sourcemap-aware-replace, with modern
* JavaScript updates, along with additional properties copied from originalMap.
*
* @param {Object} options
* @param {string} options.jsFilename The name for the file whose contents
* correspond to originalSource.
* @param {Object} options.originalMap The sourcemap for originalSource,
* prior to any replacements.
* @param {string} options.originalSource The source code, prior to any
* replacements.
* @param {string} options.replaceString A string to swap in for searchString.
* @param {string} options.searchString A string in originalSource to replace.
* Only the first occurrence will be replaced.
* @return {{source: string, map: string}} An object containing both
* originalSource with the replacement applied, and the modified originalMap.
*
* @private
*/
export async function replaceAndUpdateSourceMap({
jsFilename,
originalMap,
originalSource,
replaceString,
searchString,
}: {
jsFilename: string;
originalMap: RawSourceMap;
originalSource: string;
replaceString: string;
searchString: string;
}): Promise<{map: string; source: string}> {
const generator = new SourceMapGenerator({
file: jsFilename,
});
const consumer = await new SourceMapConsumer(originalMap);
let pos: number;
let src = originalSource;
const replacements: Array<{line: number; column: number}> = [];
let lineNum = 0;
let filePos = 0;
const lines = src.split('\n');
for (let line of lines) {
lineNum++;
let searchPos = 0;
while ((pos = line.indexOf(searchString, searchPos)) !== -1) {
src =
src.substring(0, filePos + pos) +
replaceString +
src.substring(filePos + pos + searchString.length);
line =
line.substring(0, pos) +
replaceString +
line.substring(pos + searchString.length);
replacements.push({line: lineNum, column: pos});
searchPos = pos + replaceString.length;
}
filePos += line.length + 1;
}
replacements.reverse();
consumer.eachMapping((mapping) => {
for (const replacement of replacements) {
if (
replacement.line === mapping.generatedLine &&
mapping.generatedColumn > replacement.column
) {
const offset = searchString.length - replaceString.length;
mapping.generatedColumn -= offset;
}
}
if (mapping.source) {
const newMapping = {
generated: {
line: mapping.generatedLine,
column: mapping.generatedColumn,
},
original: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
source: mapping.source,
};
return generator.addMapping(newMapping);
}
return mapping;
});
consumer.destroy();
// JSON.parse returns any.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const updatedSourceMap: RawSourceMap = Object.assign(
JSON.parse(generator.toString()),
{
names: originalMap.names,
sourceRoot: originalMap.sourceRoot,
sources: originalMap.sources,
sourcesContent: originalMap.sourcesContent,
},
);
return {
map: JSON.stringify(updatedSourceMap),
source: src,
};
}
+215
View File
@@ -0,0 +1,215 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {oneLine as ol} from 'common-tags';
import {errors} from './errors';
import {ModuleRegistry} from './module-registry';
import {RuntimeCaching} from '../types';
import {stringifyWithoutComments} from './stringify-without-comments';
/**
* Given a set of options that configures runtime caching behavior, convert it
* to the equivalent Workbox method calls.
*
* @param {ModuleRegistry} moduleRegistry
* @param {Object} options See
* https://developers.google.com/web/tools/workbox/modules/workbox-build#generateSW-runtimeCaching
* @return {string} A JSON string representing the equivalent options.
*
* @private
*/
function getOptionsString(
moduleRegistry: ModuleRegistry,
options: RuntimeCaching['options'] = {},
) {
const plugins: Array<string> = [];
const handlerOptions: {[key in keyof typeof options]: any} = {};
for (const optionName of Object.keys(options) as Array<
keyof typeof options
>) {
if (options[optionName] === undefined) {
continue;
}
switch (optionName) {
// Using a library here because JSON.stringify won't handle functions.
case 'plugins': {
plugins.push(...options.plugins!.map(stringifyWithoutComments));
break;
}
// These are the option properties that we want to pull out, so that
// they're passed to the handler constructor.
case 'cacheName':
case 'networkTimeoutSeconds':
case 'fetchOptions':
case 'matchOptions': {
handlerOptions[optionName] = options[optionName];
break;
}
// The following cases are all shorthands for creating a plugin with a
// given configuration.
case 'backgroundSync': {
const name = options.backgroundSync!.name;
const plugin = moduleRegistry.use(
'workbox-background-sync',
'BackgroundSyncPlugin',
);
let pluginCode = `new ${plugin}(${JSON.stringify(name)}`;
if (options.backgroundSync!.options) {
pluginCode += `, ${stringifyWithoutComments(
options.backgroundSync!.options,
)}`;
}
pluginCode += `)`;
plugins.push(pluginCode);
break;
}
case 'broadcastUpdate': {
const channelName = options.broadcastUpdate!.channelName;
const opts = Object.assign(
{channelName},
options.broadcastUpdate!.options,
);
const plugin = moduleRegistry.use(
'workbox-broadcast-update',
'BroadcastUpdatePlugin',
);
plugins.push(`new ${plugin}(${stringifyWithoutComments(opts)})`);
break;
}
case 'cacheableResponse': {
const plugin = moduleRegistry.use(
'workbox-cacheable-response',
'CacheableResponsePlugin',
);
plugins.push(
`new ${plugin}(${stringifyWithoutComments(
options.cacheableResponse!,
)})`,
);
break;
}
case 'expiration': {
const plugin = moduleRegistry.use(
'workbox-expiration',
'ExpirationPlugin',
);
plugins.push(
`new ${plugin}(${stringifyWithoutComments(options.expiration!)})`,
);
break;
}
case 'precacheFallback': {
const plugin = moduleRegistry.use(
'workbox-precaching',
'PrecacheFallbackPlugin',
);
plugins.push(
`new ${plugin}(${stringifyWithoutComments(
options.precacheFallback!,
)})`,
);
break;
}
case 'rangeRequests': {
const plugin = moduleRegistry.use(
'workbox-range-requests',
'RangeRequestsPlugin',
);
// There are no configuration options for the constructor.
plugins.push(`new ${plugin}()`);
break;
}
default: {
throw new Error(
// In the default case optionName is typed as 'never'.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${errors['bad-runtime-caching-config']} ${optionName}`,
);
}
}
}
if (Object.keys(handlerOptions).length > 0 || plugins.length > 0) {
const optionsString = JSON.stringify(handlerOptions).slice(1, -1);
return ol`{
${optionsString ? optionsString + ',' : ''}
plugins: [${plugins.join(', ')}]
}`;
} else {
return '';
}
}
export function runtimeCachingConverter(
moduleRegistry: ModuleRegistry,
runtimeCaching: Array<RuntimeCaching>,
): Array<string> {
return runtimeCaching
.map((entry) => {
const method = entry.method || 'GET';
if (!entry.urlPattern) {
throw new Error(errors['urlPattern-is-required']);
}
if (!entry.handler) {
throw new Error(errors['handler-is-required']);
}
if (
entry.options &&
entry.options.networkTimeoutSeconds &&
entry.handler !== 'NetworkFirst'
) {
throw new Error(errors['invalid-network-timeout-seconds']);
}
// urlPattern might be a string, a RegExp object, or a function.
// If it's a string, it needs to be quoted.
const matcher =
typeof entry.urlPattern === 'string'
? JSON.stringify(entry.urlPattern)
: entry.urlPattern;
const registerRoute = moduleRegistry.use(
'workbox-routing',
'registerRoute',
);
if (typeof entry.handler === 'string') {
const optionsString = getOptionsString(moduleRegistry, entry.options);
const handler = moduleRegistry.use('workbox-strategies', entry.handler);
const strategyString = `new ${handler}(${optionsString})`;
return `${registerRoute}(${matcher.toString()}, ${strategyString}, '${method}');\n`;
} else if (typeof entry.handler === 'function') {
return `${registerRoute}(${matcher.toString()}, ${entry.handler.toString()}, '${method}');\n`;
}
// '' will be filtered out.
return '';
})
.filter((entry) => Boolean(entry));
}
+23
View File
@@ -0,0 +1,23 @@
/*
Copyright 2021 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import objectStringify from 'stringify-object';
import stripComments from 'strip-comments';
export function stringifyWithoutComments(obj: {[key: string]: any}): string {
return objectStringify(obj, {
// See https://github.com/yeoman/stringify-object#transformobject-property-originalresult
transform: (_obj: {[key: string]: any}, _prop, str) => {
if (typeof _prop !== 'symbol' && typeof _obj[_prop] === 'function') {
// Can't typify correctly stripComments
return stripComments(str); // eslint-disable-line
}
return str;
},
});
}
+162
View File
@@ -0,0 +1,162 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {
BasePartial,
FileDetails,
ManifestEntry,
ManifestTransform,
} from '../types';
import {additionalManifestEntriesTransform} from './additional-manifest-entries-transform';
import {errors} from './errors';
import {maximumSizeTransform} from './maximum-size-transform';
import {modifyURLPrefixTransform} from './modify-url-prefix-transform';
import {noRevisionForURLsMatchingTransform} from './no-revision-for-urls-matching-transform';
/**
* A `ManifestTransform` function can be used to modify the modify the `url` or
* `revision` properties of some or all of the
* {@link workbox-build.ManifestEntry} in the manifest.
*
* Deleting the `revision` property of an entry will cause
* the corresponding `url` to be precached without cache-busting parameters
* applied, which is to say, it implies that the URL itself contains
* proper versioning info. If the `revision` property is present, it must be
* set to a string.
*
* @example A transformation that prepended the origin of a CDN for any
* URL starting with '/assets/' could be implemented as:
*
* const cdnTransform = async (manifestEntries) => {
* const manifest = manifestEntries.map(entry => {
* const cdnOrigin = 'https://example.com';
* if (entry.url.startsWith('/assets/')) {
* entry.url = cdnOrigin + entry.url;
* }
* return entry;
* });
* return {manifest, warnings: []};
* };
*
* @example A transformation that nulls the revision field when the
* URL contains an 8-character hash surrounded by '.', indicating that it
* already contains revision information:
*
* const removeRevisionTransform = async (manifestEntries) => {
* const manifest = manifestEntries.map(entry => {
* const hashRegExp = /\.\w{8}\./;
* if (entry.url.match(hashRegExp)) {
* entry.revision = null;
* }
* return entry;
* });
* return {manifest, warnings: []};
* };
*
* @callback ManifestTransform
* @param {Array<workbox-build.ManifestEntry>} manifestEntries The full
* array of entries, prior to the current transformation.
* @param {Object} [compilation] When used in the webpack plugins, this param
* will be set to the current `compilation`.
* @return {Promise<workbox-build.ManifestTransformResult>}
* The array of entries with the transformation applied, and optionally, any
* warnings that should be reported back to the build tool.
*
* @memberof workbox-build
*/
interface ManifestTransformResultWithWarnings {
count: number;
size: number;
manifestEntries: ManifestEntry[];
warnings: string[];
}
export async function transformManifest({
additionalManifestEntries,
dontCacheBustURLsMatching,
fileDetails,
manifestTransforms,
maximumFileSizeToCacheInBytes,
modifyURLPrefix,
transformParam,
}: BasePartial & {
fileDetails: Array<FileDetails>;
// When this is called by the webpack plugin, transformParam will be the
// current webpack compilation.
transformParam?: unknown;
}): Promise<ManifestTransformResultWithWarnings> {
const allWarnings: Array<string> = [];
// Take the array of fileDetail objects and convert it into an array of
// {url, revision, size} objects, with \ replaced with /.
const normalizedManifest = fileDetails.map((fileDetails) => {
return {
url: fileDetails.file.replace(/\\/g, '/'),
revision: fileDetails.hash,
size: fileDetails.size,
};
});
const transformsToApply: Array<ManifestTransform> = [];
if (maximumFileSizeToCacheInBytes) {
transformsToApply.push(maximumSizeTransform(maximumFileSizeToCacheInBytes));
}
if (modifyURLPrefix) {
transformsToApply.push(modifyURLPrefixTransform(modifyURLPrefix));
}
if (dontCacheBustURLsMatching) {
transformsToApply.push(
noRevisionForURLsMatchingTransform(dontCacheBustURLsMatching),
);
}
// Run any manifestTransforms functions second-to-last.
if (manifestTransforms) {
transformsToApply.push(...manifestTransforms);
}
// Run additionalManifestEntriesTransform last.
if (additionalManifestEntries) {
transformsToApply.push(
additionalManifestEntriesTransform(additionalManifestEntries),
);
}
let transformedManifest: Array<ManifestEntry & {size: number}> =
normalizedManifest;
for (const transform of transformsToApply) {
const result = await transform(transformedManifest, transformParam);
if (!('manifest' in result)) {
throw new Error(errors['bad-manifest-transforms-return-value']);
}
transformedManifest = result.manifest;
allWarnings.push(...(result.warnings || []));
}
// Generate some metadata about the manifest before we clear out the size
// properties from each entry.
const count = transformedManifest.length;
let size = 0;
for (const manifestEntry of transformedManifest as Array<
ManifestEntry & {size?: number}
>) {
size += manifestEntry.size || 0;
delete manifestEntry.size;
}
return {
count,
size,
manifestEntries: transformedManifest as Array<ManifestEntry>,
warnings: allWarnings,
};
}
+38
View File
@@ -0,0 +1,38 @@
/*
Copyright 2021 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import fse from 'fs-extra';
import upath from 'upath';
import {errors} from './errors';
export function translateURLToSourcemapPaths(
url: string | null,
swSrc: string,
swDest: string,
): {
destPath: string | undefined;
srcPath: string | undefined;
warning: string | undefined;
} {
let destPath: string | undefined = undefined;
let srcPath: string | undefined = undefined;
let warning: string | undefined = undefined;
if (url && !url.startsWith('data:')) {
const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url);
if (fse.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
destPath = upath.resolve(upath.dirname(swDest), url);
} else {
warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`;
}
}
return {destPath, srcPath, warning};
}
+235
View File
@@ -0,0 +1,235 @@
/*
Copyright 2021 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import {betterAjvErrors} from '@apideck/better-ajv-errors';
import {oneLine as ol} from 'common-tags';
import Ajv, {JSONSchemaType} from 'ajv';
import {errors} from './errors';
import {
GenerateSWOptions,
GetManifestOptions,
InjectManifestOptions,
WebpackGenerateSWOptions,
WebpackInjectManifestOptions,
} from '../types';
type MethodNames =
| 'GenerateSW'
| 'GetManifest'
| 'InjectManifest'
| 'WebpackGenerateSW'
| 'WebpackInjectManifest';
const ajv = new Ajv({
useDefaults: true,
});
const DEFAULT_EXCLUDE_VALUE = [/\.map$/, /^manifest.*\.js$/];
export class WorkboxConfigError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
// Some methods need to do follow-up validation using the JSON schema,
// so return both the validated options and then schema.
function validate<T>(
input: unknown,
methodName: MethodNames,
): [T, JSONSchemaType<T>] {
// Don't mutate input: https://github.com/GoogleChrome/workbox/issues/2158
const inputCopy = Object.assign({}, input);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const jsonSchema: JSONSchemaType<T> = require(`../schema/${methodName}Options.json`);
const validate = ajv.compile(jsonSchema);
if (validate(inputCopy)) {
// All methods support manifestTransforms, so validate it here.
ensureValidManifestTransforms(inputCopy);
return [inputCopy, jsonSchema];
}
const betterErrors = betterAjvErrors({
basePath: methodName,
data: input,
errors: validate.errors,
// This is needed as JSONSchema6 is expected, but JSONSchemaType works.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
schema: jsonSchema as any,
});
const messages = betterErrors.map(
(err) => ol`[${err.path}] ${err.message}.
${err.suggestion ? err.suggestion : ''}`,
);
throw new WorkboxConfigError(messages.join('\n\n'));
}
function ensureValidManifestTransforms(
options:
| GenerateSWOptions
| GetManifestOptions
| InjectManifestOptions
| WebpackGenerateSWOptions
| WebpackInjectManifestOptions,
): void {
if (
'manifestTransforms' in options &&
!(
Array.isArray(options.manifestTransforms) &&
options.manifestTransforms.every((item) => typeof item === 'function')
)
) {
throw new WorkboxConfigError(errors['manifest-transforms']);
}
}
function ensureValidNavigationPreloadConfig(
options: GenerateSWOptions | WebpackGenerateSWOptions,
): void {
if (
options.navigationPreload &&
(!Array.isArray(options.runtimeCaching) ||
options.runtimeCaching.length === 0)
) {
throw new WorkboxConfigError(errors['nav-preload-runtime-caching']);
}
}
function ensureValidCacheExpiration(
options: GenerateSWOptions | WebpackGenerateSWOptions,
): void {
for (const runtimeCaching of options.runtimeCaching || []) {
if (
runtimeCaching.options?.expiration &&
!runtimeCaching.options?.cacheName
) {
throw new WorkboxConfigError(errors['cache-name-required']);
}
}
}
function ensureValidRuntimeCachingOrGlobDirectory(
options: GenerateSWOptions,
): void {
if (
!options.globDirectory &&
(!Array.isArray(options.runtimeCaching) ||
options.runtimeCaching.length === 0)
) {
throw new WorkboxConfigError(
errors['no-manifest-entries-or-runtime-caching'],
);
}
}
// This is... messy, because we can't rely on the built-in ajv validation for
// runtimeCaching.handler, as it needs to accept {} (i.e. any) due to
// https://github.com/GoogleChrome/workbox/pull/2899
// So we need to perform validation when a string (not a function) is used.
function ensureValidStringHandler(
options: GenerateSWOptions | WebpackGenerateSWOptions,
jsonSchema: JSONSchemaType<GenerateSWOptions | WebpackGenerateSWOptions>,
): void {
let validHandlers: Array<string> = [];
/* eslint-disable */
for (const handler of jsonSchema.definitions?.RuntimeCaching?.properties
?.handler?.anyOf || []) {
if ('enum' in handler) {
validHandlers = handler.enum;
break;
}
}
/* eslint-enable */
for (const runtimeCaching of options.runtimeCaching || []) {
if (
typeof runtimeCaching.handler === 'string' &&
!validHandlers.includes(runtimeCaching.handler)
) {
throw new WorkboxConfigError(
errors['invalid-handler-string'] + runtimeCaching.handler,
);
}
}
}
export function validateGenerateSWOptions(input: unknown): GenerateSWOptions {
const [validatedOptions, jsonSchema] = validate<GenerateSWOptions>(
input,
'GenerateSW',
);
ensureValidNavigationPreloadConfig(validatedOptions);
ensureValidCacheExpiration(validatedOptions);
ensureValidRuntimeCachingOrGlobDirectory(validatedOptions);
ensureValidStringHandler(validatedOptions, jsonSchema);
return validatedOptions;
}
export function validateGetManifestOptions(input: unknown): GetManifestOptions {
const [validatedOptions] = validate<GetManifestOptions>(input, 'GetManifest');
return validatedOptions;
}
export function validateInjectManifestOptions(
input: unknown,
): InjectManifestOptions {
const [validatedOptions] = validate<InjectManifestOptions>(
input,
'InjectManifest',
);
return validatedOptions;
}
// The default `exclude: [/\.map$/, /^manifest.*\.js$/]` value can't be
// represented in the JSON schema, so manually set it for the webpack options.
export function validateWebpackGenerateSWOptions(
input: unknown,
): WebpackGenerateSWOptions {
const inputWithExcludeDefault = Object.assign(
{
// Make a copy, as exclude can be mutated when used.
exclude: Array.from(DEFAULT_EXCLUDE_VALUE),
},
input,
);
const [validatedOptions, jsonSchema] = validate<WebpackGenerateSWOptions>(
inputWithExcludeDefault,
'WebpackGenerateSW',
);
ensureValidNavigationPreloadConfig(validatedOptions);
ensureValidCacheExpiration(validatedOptions);
ensureValidStringHandler(validatedOptions, jsonSchema);
return validatedOptions;
}
export function validateWebpackInjectManifestOptions(
input: unknown,
): WebpackInjectManifestOptions {
const inputWithExcludeDefault = Object.assign(
{
// Make a copy, as exclude can be mutated when used.
exclude: Array.from(DEFAULT_EXCLUDE_VALUE),
},
input,
);
const [validatedOptions] = validate<WebpackInjectManifestOptions>(
inputWithExcludeDefault,
'WebpackInjectManifest',
);
return validatedOptions;
}
+96
View File
@@ -0,0 +1,96 @@
/*
Copyright 2018 Google LLC
Use of this source code is governed by an MIT-style
license that can be found in the LICENSE file or at
https://opensource.org/licenses/MIT.
*/
import fse from 'fs-extra';
import upath from 'upath';
import {bundle} from './bundle';
import {errors} from './errors';
import {GenerateSWOptions, ManifestEntry} from '../types';
import {populateSWTemplate} from './populate-sw-template';
export async function writeSWUsingDefaultTemplate({
babelPresetEnvTargets,
cacheId,
cleanupOutdatedCaches,
clientsClaim,
directoryIndex,
disableDevLogs,
ignoreURLParametersMatching,
importScripts,
inlineWorkboxRuntime,
manifestEntries,
mode,
navigateFallback,
navigateFallbackDenylist,
navigateFallbackAllowlist,
navigationPreload,
offlineGoogleAnalytics,
runtimeCaching,
skipWaiting,
sourcemap,
swDest,
}: GenerateSWOptions & {manifestEntries: Array<ManifestEntry>}): Promise<
Array<string>
> {
const outputDir = upath.dirname(swDest);
try {
await fse.mkdirp(outputDir);
} catch (error) {
throw new Error(
`${errors['unable-to-make-sw-directory']}. ` +
`'${error instanceof Error && error.message ? error.message : ''}'`,
);
}
const unbundledCode = populateSWTemplate({
cacheId,
cleanupOutdatedCaches,
clientsClaim,
directoryIndex,
disableDevLogs,
ignoreURLParametersMatching,
importScripts,
manifestEntries,
navigateFallback,
navigateFallbackDenylist,
navigateFallbackAllowlist,
navigationPreload,
offlineGoogleAnalytics,
runtimeCaching,
skipWaiting,
});
try {
const files = await bundle({
babelPresetEnvTargets,
inlineWorkboxRuntime,
mode,
sourcemap,
swDest,
unbundledCode,
});
const filePaths: Array<string> = [];
for (const file of files) {
const filePath = upath.resolve(file.name);
filePaths.push(filePath);
await fse.writeFile(filePath, file.contents);
}
return filePaths;
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code === 'EISDIR') {
// See https://github.com/GoogleChrome/workbox/issues/612
throw new Error(errors['sw-write-failure-directory']);
}
throw new Error(`${errors['sw-write-failure']} '${err.message}'`);
}
}