This commit is contained in:
+58
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
+34
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}'`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user