Repository: GoogleChrome/workbox
Branch: v7
Commit: 1893b3f6ca3d
Files: 717
Total size: 1.9 MB
Directory structure:
gitextract_424215io/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── pull-request.yml
│ └── scorecards-analysis.yml
├── .gitignore
├── .husky/
│ ├── .gitignore
│ ├── pre-commit
│ └── pre-push
├── .ncurc.js
├── .nvmrc
├── .prettierignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cdn-details.json
├── demos/
│ ├── README.md
│ └── src/
│ ├── workbox-background-sync-demo/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public/
│ │ │ └── example.txt
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-broadcast-update-demo/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-cacheable-response/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── server.js
│ │ └── sw.js
│ ├── workbox-core/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-expiration/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-google-analytics/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-navigation-preload/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-precaching/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── hello-world.1234.txt
│ │ │ ├── hello-world.5678.txt
│ │ │ └── test-file.txt
│ │ ├── sw-1.js
│ │ ├── sw-2.js
│ │ └── updateServer.js
│ ├── workbox-range-requests/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-routing/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-strategies/
│ │ ├── index.html
│ │ ├── public/
│ │ │ ├── cache-first.txt
│ │ │ ├── network-first.txt
│ │ │ ├── network-only.txt
│ │ │ └── stale-while-revalidate.txt
│ │ ├── sw.js
│ │ └── updateServer.js
│ ├── workbox-streams/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── server.js
│ │ └── sw.js
│ ├── workbox-sw/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── sw.js
│ │ └── updateServer.js
│ └── workbox-window/
│ ├── index.html
│ ├── package.json
│ ├── sw.js
│ └── updateServer.js
├── gulp-tasks/
│ ├── analyze-properties.js
│ ├── build-node-packages.js
│ ├── build-packages.js
│ ├── build-sw-packages.js
│ ├── build-window-packages.js
│ ├── build.js
│ ├── docs.js
│ ├── lint.js
│ ├── publish-cdn.js
│ ├── publish-github.js
│ ├── publish-glitch.js
│ ├── publish-lerna.js
│ ├── publish.js
│ ├── test-integration.js
│ ├── test-node.js
│ ├── test-server.js
│ ├── test.js
│ ├── transpile-typescript.js
│ └── utils/
│ ├── analyse-properties.js
│ ├── cdn-helper.js
│ ├── constants.js
│ ├── get-packages.js
│ ├── github-helper.js
│ ├── node-projects-babel.config.json
│ ├── output-filename-to-package-map.js
│ ├── package-runner.js
│ ├── pkg-path-to-name.js
│ ├── publish-helpers.js
│ ├── rollup-helper.js
│ ├── version-module.js
│ └── versioned-cdn-url.js
├── gulpfile.js
├── infra/
│ ├── pr-bot/
│ │ └── aggregate-size-plugin.js
│ ├── templates/
│ │ └── reference-docs/
│ │ └── jsdoc/
│ │ ├── lang/
│ │ │ └── en.yaml
│ │ ├── lib/
│ │ │ └── publishjob.js
│ │ ├── publish.js
│ │ ├── static/
│ │ │ └── jsdoc.css
│ │ └── views/
│ │ ├── augments.hbs
│ │ ├── classes-links.hbs
│ │ ├── details-table-row.hbs
│ │ ├── details-table.hbs
│ │ ├── implements.hbs
│ │ ├── index-all.hbs
│ │ ├── index.hbs
│ │ ├── layout.hbs
│ │ ├── params.hbs
│ │ ├── properties.hbs
│ │ ├── see.hbs
│ │ ├── signature.hbs
│ │ ├── symbol-content.hbs
│ │ ├── symbol-detail.hbs
│ │ ├── symbol-header.hbs
│ │ ├── symbol-index-section.hbs
│ │ ├── symbol-index.hbs
│ │ ├── symbol-labels.hbs
│ │ ├── symbol-overview.hbs
│ │ └── toc-yaml.hbs
│ ├── testing/
│ │ ├── activate-and-control.js
│ │ ├── activate-sw-safari.js
│ │ ├── auto-stub-logger.mjs
│ │ ├── clean-sw.js
│ │ ├── comlink/
│ │ │ ├── node-interface.js
│ │ │ ├── sw-interface.js
│ │ │ └── window-interface.js
│ │ ├── confirm-directory-contains.js
│ │ ├── env-it.js
│ │ ├── expectError.js
│ │ ├── generate-variant-tests.js
│ │ ├── helpers/
│ │ │ ├── compareResponses.mjs
│ │ │ ├── extendable-event-utils.mjs
│ │ │ ├── generateOpaqueResponse.mjs
│ │ │ ├── generateUniqueResponse.mjs
│ │ │ └── sleep.mjs
│ │ ├── server/
│ │ │ ├── cross-origin-server.js
│ │ │ ├── index.js
│ │ │ ├── request-counter.js
│ │ │ ├── routes/
│ │ │ │ ├── build-file.js
│ │ │ │ ├── comlink.js
│ │ │ │ ├── integration-html.js
│ │ │ │ ├── sw-bundle.js
│ │ │ │ ├── templates-update.js
│ │ │ │ ├── templates.js
│ │ │ │ ├── test-sw.js
│ │ │ │ ├── test-window.js
│ │ │ │ ├── unique-etag.js
│ │ │ │ └── unique-value.js
│ │ │ ├── static/
│ │ │ │ └── integration.html
│ │ │ ├── template-data.js
│ │ │ └── templates/
│ │ │ ├── integration.html.njk
│ │ │ ├── sw-clients-claim.js.njk
│ │ │ ├── sw-no-skip-waiting.js.njk
│ │ │ ├── sw-script-version.js.njk
│ │ │ ├── sw-skip-waiting-deferred.js.njk
│ │ │ ├── sw-skip-waiting-on-message.js.njk
│ │ │ ├── sw-skip-waiting.js.njk
│ │ │ ├── sw-window-ready.js.njk
│ │ │ ├── test-sw-runner.js.njk
│ │ │ ├── test-sw.html.njk
│ │ │ └── test-window.html.njk
│ │ ├── validator/
│ │ │ └── service-worker-runtime.js
│ │ ├── wait-until.js
│ │ ├── webdriver/
│ │ │ ├── IframeManager.js
│ │ │ ├── executeAsyncAndCatch.js
│ │ │ ├── runUnitTests.js
│ │ │ ├── unregisterAllSWs.js
│ │ │ └── windowLoaded.js
│ │ └── webpack-build-check.js
│ ├── type-overrides.d.ts
│ └── utils/
│ ├── AsyncDebounce.js
│ └── log-helper.js
├── javascript.eslintrc.js
├── jsdoc.conf
├── lerna.json
├── package.json
├── packages/
│ ├── workbox-background-sync/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── BackgroundSyncPlugin.ts
│ │ │ ├── Queue.ts
│ │ │ ├── QueueStore.ts
│ │ │ ├── StorableRequest.ts
│ │ │ ├── _version.ts
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── QueueDb.ts
│ │ │ ├── QueueStore.ts
│ │ │ └── StorableRequest.ts
│ │ └── tsconfig.json
│ ├── workbox-broadcast-update/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── BroadcastCacheUpdate.ts
│ │ │ ├── BroadcastUpdatePlugin.ts
│ │ │ ├── _version.ts
│ │ │ ├── index.ts
│ │ │ ├── responsesAreSame.ts
│ │ │ └── utils/
│ │ │ └── constants.ts
│ │ └── tsconfig.json
│ ├── workbox-build/
│ │ ├── .ncurc.js
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _types.js
│ │ │ ├── cdn-details.json
│ │ │ ├── generate-sw.ts
│ │ │ ├── get-manifest.ts
│ │ │ ├── index.ts
│ │ │ ├── inject-manifest.ts
│ │ │ ├── lib/
│ │ │ │ ├── additional-manifest-entries-transform.ts
│ │ │ │ ├── bundle.ts
│ │ │ │ ├── cdn-utils.ts
│ │ │ │ ├── copy-workbox-libraries.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── escape-regexp.ts
│ │ │ │ ├── get-composite-details.ts
│ │ │ │ ├── get-file-details.ts
│ │ │ │ ├── get-file-hash.ts
│ │ │ │ ├── get-file-manifest-entries.ts
│ │ │ │ ├── get-file-size.ts
│ │ │ │ ├── get-source-map-url.ts
│ │ │ │ ├── get-string-details.ts
│ │ │ │ ├── get-string-hash.ts
│ │ │ │ ├── maximum-size-transform.ts
│ │ │ │ ├── modify-url-prefix-transform.ts
│ │ │ │ ├── module-registry.ts
│ │ │ │ ├── no-revision-for-urls-matching-transform.ts
│ │ │ │ ├── populate-sw-template.ts
│ │ │ │ ├── rebase-path.ts
│ │ │ │ ├── replace-and-update-source-map.ts
│ │ │ │ ├── runtime-caching-converter.ts
│ │ │ │ ├── stringify-without-comments.ts
│ │ │ │ ├── transform-manifest.ts
│ │ │ │ ├── translate-url-to-sourcemap-paths.ts
│ │ │ │ ├── validate-options.ts
│ │ │ │ └── write-sw-using-default-template.ts
│ │ │ ├── rollup-plugin-off-main-thread.d.ts
│ │ │ ├── schema/
│ │ │ │ ├── GenerateSWOptions.json
│ │ │ │ ├── GetManifestOptions.json
│ │ │ │ ├── InjectManifestOptions.json
│ │ │ │ ├── WebpackGenerateSWOptions.json
│ │ │ │ └── WebpackInjectManifestOptions.json
│ │ │ ├── strip-comments.d.ts
│ │ │ ├── templates/
│ │ │ │ └── sw-template.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── workbox-cacheable-response/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── CacheableResponse.ts
│ │ │ ├── CacheableResponsePlugin.ts
│ │ │ ├── _version.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── workbox-cli/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── app.ts
│ │ │ ├── bin.ts
│ │ │ └── lib/
│ │ │ ├── cleanup-stack-trace.ts
│ │ │ ├── constants.ts
│ │ │ ├── errors.ts
│ │ │ ├── help-text.ts
│ │ │ ├── logger.ts
│ │ │ ├── questions/
│ │ │ │ ├── ask-config-location.ts
│ │ │ │ ├── ask-extensions-to-cache.ts
│ │ │ │ ├── ask-questions.ts
│ │ │ │ ├── ask-root-of-web-app.ts
│ │ │ │ ├── ask-start_url-query-params.ts
│ │ │ │ ├── ask-sw-dest.ts
│ │ │ │ └── ask-sw-src.ts
│ │ │ ├── read-config.ts
│ │ │ └── run-wizard.ts
│ │ └── tsconfig.json
│ ├── workbox-core/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _private/
│ │ │ │ ├── Deferred.ts
│ │ │ │ ├── WorkboxError.ts
│ │ │ │ ├── assert.ts
│ │ │ │ ├── cacheMatchIgnoreParams.ts
│ │ │ │ ├── cacheNames.ts
│ │ │ │ ├── canConstructReadableStream.ts
│ │ │ │ ├── canConstructResponseFromBodyStream.ts
│ │ │ │ ├── dontWaitFor.ts
│ │ │ │ ├── executeQuotaErrorCallbacks.ts
│ │ │ │ ├── getFriendlyURL.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── resultingClientExists.ts
│ │ │ │ ├── timeout.ts
│ │ │ │ └── waitUntil.ts
│ │ │ ├── _private.ts
│ │ │ ├── _version.ts
│ │ │ ├── cacheNames.ts
│ │ │ ├── clientsClaim.ts
│ │ │ ├── copyResponse.ts
│ │ │ ├── index.ts
│ │ │ ├── models/
│ │ │ │ ├── messages/
│ │ │ │ │ ├── messageGenerator.ts
│ │ │ │ │ └── messages.ts
│ │ │ │ ├── pluginEvents.ts
│ │ │ │ └── quotaErrorCallbacks.ts
│ │ │ ├── registerQuotaErrorCallback.ts
│ │ │ ├── setCacheNameDetails.ts
│ │ │ ├── skipWaiting.ts
│ │ │ ├── types.ts
│ │ │ └── utils/
│ │ │ ├── pluginUtils.ts
│ │ │ └── welcome.ts
│ │ └── tsconfig.json
│ ├── workbox-expiration/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── CacheExpiration.ts
│ │ │ ├── ExpirationPlugin.ts
│ │ │ ├── _version.ts
│ │ │ ├── index.ts
│ │ │ └── models/
│ │ │ └── CacheTimestampsModel.ts
│ │ └── tsconfig.json
│ ├── workbox-google-analytics/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _version.ts
│ │ │ ├── index.ts
│ │ │ ├── initialize.ts
│ │ │ └── utils/
│ │ │ └── constants.ts
│ │ └── tsconfig.json
│ ├── workbox-navigation-preload/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _version.ts
│ │ │ ├── disable.ts
│ │ │ ├── enable.ts
│ │ │ ├── index.ts
│ │ │ └── isSupported.ts
│ │ └── tsconfig.json
│ ├── workbox-precaching/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── PrecacheController.ts
│ │ │ ├── PrecacheFallbackPlugin.ts
│ │ │ ├── PrecacheRoute.ts
│ │ │ ├── PrecacheStrategy.ts
│ │ │ ├── _types.ts
│ │ │ ├── _version.ts
│ │ │ ├── addPlugins.ts
│ │ │ ├── addRoute.ts
│ │ │ ├── cleanupOutdatedCaches.ts
│ │ │ ├── createHandlerBoundToURL.ts
│ │ │ ├── getCacheKeyForURL.ts
│ │ │ ├── index.ts
│ │ │ ├── matchPrecache.ts
│ │ │ ├── precache.ts
│ │ │ ├── precacheAndRoute.ts
│ │ │ └── utils/
│ │ │ ├── PrecacheCacheKeyPlugin.ts
│ │ │ ├── PrecacheInstallReportPlugin.ts
│ │ │ ├── createCacheKey.ts
│ │ │ ├── deleteOutdatedCaches.ts
│ │ │ ├── generateURLVariations.ts
│ │ │ ├── getCacheKeyForURL.ts
│ │ │ ├── getOrCreatePrecacheController.ts
│ │ │ ├── printCleanupDetails.ts
│ │ │ ├── printInstallDetails.ts
│ │ │ └── removeIgnoredSearchParams.ts
│ │ └── tsconfig.json
│ ├── workbox-range-requests/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── RangeRequestsPlugin.ts
│ │ │ ├── _version.ts
│ │ │ ├── createPartialResponse.ts
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ ├── calculateEffectiveBoundaries.ts
│ │ │ └── parseRangeHeader.ts
│ │ └── tsconfig.json
│ ├── workbox-recipes/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _version.ts
│ │ │ ├── googleFontsCache.ts
│ │ │ ├── imageCache.ts
│ │ │ ├── index.ts
│ │ │ ├── offlineFallback.ts
│ │ │ ├── pageCache.ts
│ │ │ ├── staticResourceCache.ts
│ │ │ └── warmStrategyCache.ts
│ │ └── tsconfig.json
│ ├── workbox-routing/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── NavigationRoute.ts
│ │ │ ├── RegExpRoute.ts
│ │ │ ├── Route.ts
│ │ │ ├── Router.ts
│ │ │ ├── _types.ts
│ │ │ ├── _version.ts
│ │ │ ├── index.ts
│ │ │ ├── registerRoute.ts
│ │ │ ├── setCatchHandler.ts
│ │ │ ├── setDefaultHandler.ts
│ │ │ └── utils/
│ │ │ ├── constants.ts
│ │ │ ├── getOrCreateDefaultRouter.ts
│ │ │ └── normalizeHandler.ts
│ │ └── tsconfig.json
│ ├── workbox-strategies/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── CacheFirst.ts
│ │ │ ├── CacheOnly.ts
│ │ │ ├── NetworkFirst.ts
│ │ │ ├── NetworkOnly.ts
│ │ │ ├── StaleWhileRevalidate.ts
│ │ │ ├── Strategy.ts
│ │ │ ├── StrategyHandler.ts
│ │ │ ├── _version.ts
│ │ │ ├── index.ts
│ │ │ ├── plugins/
│ │ │ │ └── cacheOkAndOpaquePlugin.ts
│ │ │ └── utils/
│ │ │ └── messages.ts
│ │ └── tsconfig.json
│ ├── workbox-streams/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _types.ts
│ │ │ ├── _version.ts
│ │ │ ├── concatenate.ts
│ │ │ ├── concatenateToResponse.ts
│ │ │ ├── index.ts
│ │ │ ├── isSupported.ts
│ │ │ ├── strategy.ts
│ │ │ └── utils/
│ │ │ └── createHeaders.ts
│ │ └── tsconfig.json
│ ├── workbox-sw/
│ │ ├── README.md
│ │ ├── _types.mjs
│ │ ├── _version.mjs
│ │ ├── controllers/
│ │ │ └── WorkboxSW.mjs
│ │ ├── index.mjs
│ │ └── package.json
│ ├── workbox-webpack-plugin/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── generate-sw.ts
│ │ │ ├── index.ts
│ │ │ ├── inject-manifest.ts
│ │ │ └── lib/
│ │ │ ├── get-asset-hash.ts
│ │ │ ├── get-manifest-entries-from-compilation.ts
│ │ │ ├── get-script-files-for-chunks.ts
│ │ │ ├── get-sourcemap-asset-name.ts
│ │ │ ├── relative-to-output-path.ts
│ │ │ └── resolve-webpack-url.ts
│ │ └── tsconfig.json
│ └── workbox-window/
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── Workbox.ts
│ │ ├── _version.ts
│ │ ├── index.ts
│ │ ├── messageSW.ts
│ │ └── utils/
│ │ ├── WorkboxEvent.ts
│ │ ├── WorkboxEventTarget.ts
│ │ └── urlsMatch.ts
│ └── tsconfig.json
├── prettier.config.js
├── test/
│ ├── all/
│ │ └── node/
│ │ ├── test-exports.js
│ │ ├── test-jsdocs.js
│ │ ├── test-package.js
│ │ ├── test-prod-builds.js
│ │ └── test-yarn-installation.js
│ ├── workbox-background-sync/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ └── basic-example/
│ │ │ ├── example.txt
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── lib/
│ │ │ ├── test-QueueDb.mjs
│ │ │ ├── test-QueueStore.mjs
│ │ │ └── test-StorableRequest.mjs
│ │ ├── test-BackgroundSyncPlugin.mjs
│ │ └── test-Queue.mjs
│ ├── workbox-broadcast-update/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── test-BroadcastCacheUpdate.mjs
│ │ ├── test-BroadcastUpdatePlugin.mjs
│ │ └── test-responsesAreSame.mjs
│ ├── workbox-build/
│ │ ├── node/
│ │ │ ├── dependency-check.js
│ │ │ ├── generate-sw.js
│ │ │ ├── get-manifest.js
│ │ │ ├── inject-manifest.js
│ │ │ └── lib/
│ │ │ ├── additional-manifest-entries-transform.js
│ │ │ ├── bundle.js
│ │ │ ├── cdn-utils.js
│ │ │ ├── copy-workbox-libraries.js
│ │ │ ├── escape-regexp.js
│ │ │ ├── get-composite-details.js
│ │ │ ├── get-file-details.js
│ │ │ ├── get-file-hash.js
│ │ │ ├── get-file-manifest-entries.js
│ │ │ ├── get-file-size.js
│ │ │ ├── get-string-details.js
│ │ │ ├── get-string-hash.js
│ │ │ ├── modify-url-prefix-transform.js
│ │ │ ├── module-registry.js
│ │ │ ├── no-revision-for-urls-matching-transform.js
│ │ │ ├── populate-sw-template.js
│ │ │ ├── replace-and-update-source-map.js
│ │ │ ├── runtime-caching-converter.js
│ │ │ ├── transform-manifest.js
│ │ │ ├── translate-url-to-sourcemap-paths.js
│ │ │ ├── validate-options.js
│ │ │ └── write-sw-using-default-template.js
│ │ └── static/
│ │ ├── example-project-1/
│ │ │ ├── .hidden-directory/
│ │ │ │ ├── hello.html
│ │ │ │ └── hello.js
│ │ │ ├── index.html
│ │ │ ├── page-1.html
│ │ │ ├── page-2.html
│ │ │ ├── styles/
│ │ │ │ ├── stylesheet-1.css
│ │ │ │ └── stylesheet-2.css
│ │ │ └── webpackEntry.js
│ │ └── sw-injections/
│ │ ├── bad-multiple-injection.js
│ │ ├── bad-no-injection.js
│ │ ├── basic-with-invalid-sourcemap.js.nolint
│ │ ├── basic-with-sourcemap-data-url.js.nolint
│ │ ├── basic-with-sourcemap.js.nolint
│ │ ├── basic.js
│ │ ├── custom-injection-point.js
│ │ ├── multiple-calls.js
│ │ ├── precache-and-route-options.js
│ │ └── sample-import.js
│ ├── workbox-cacheable-response/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ └── cacheable-response-plugin/
│ │ │ ├── example-1.txt
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── test-CacheableResponse.mjs
│ │ └── test-CacheableResponsePlugin.mjs
│ ├── workbox-cli/
│ │ └── node/
│ │ ├── app.js
│ │ ├── dependency-check.js
│ │ └── lib/
│ │ ├── cleanup-stack-trace.js
│ │ ├── help-text.js
│ │ ├── logger.js
│ │ ├── questions/
│ │ │ ├── ask-config-location.js
│ │ │ ├── ask-extensions-to-cache.js
│ │ │ ├── ask-questions.js
│ │ │ ├── ask-root-of-web-app.js
│ │ │ ├── ask-start_url-query-params.js
│ │ │ ├── ask-sw-dest.js
│ │ │ └── ask-sw-src.js
│ │ └── run-wizard.js
│ ├── workbox-core/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ ├── core-in-browser/
│ │ │ │ ├── index.html
│ │ │ │ └── sw.js
│ │ │ └── logger.html
│ │ └── sw/
│ │ ├── _private/
│ │ │ ├── test-Deferred.mjs
│ │ │ ├── test-assert.mjs
│ │ │ ├── test-cacheMatchIgnoreParams.mjs
│ │ │ ├── test-executeQuotaErrorCallbacks.mjs
│ │ │ ├── test-getFriendlyURL.mjs
│ │ │ ├── test-logger.mjs
│ │ │ ├── test-resultingClientExists.mjs
│ │ │ ├── test-timeout.mjs
│ │ │ └── test-waitUntil.mjs
│ │ ├── models/
│ │ │ └── messages/
│ │ │ └── test-messageGenerator.mjs
│ │ ├── test-cacheNames.mjs
│ │ ├── test-clientsClaim.mjs
│ │ ├── test-copyResponse.mjs
│ │ ├── test-registerQuotaErrorCallback.mjs
│ │ └── test-skipWaiting.mjs
│ ├── workbox-expiration/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ ├── expiration-plugin/
│ │ │ │ ├── example-1.txt
│ │ │ │ ├── example-2.txt
│ │ │ │ ├── sw-deletion.js
│ │ │ │ ├── sw-max-age-seconds.js
│ │ │ │ └── sw-max-entries.js
│ │ │ └── isURLExpired.html
│ │ └── sw/
│ │ ├── test-CacheExpiration.mjs
│ │ ├── test-CacheTimestampsModel.mjs
│ │ └── test-ExpirationPlugin.mjs
│ ├── workbox-google-analytics/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ └── basic-example/
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ └── sw/
│ │ └── test-initialize.mjs
│ ├── workbox-navigation-preload/
│ │ ├── integration/
│ │ │ ├── test-disable.js
│ │ │ ├── test-enable.js
│ │ │ └── test-sw.js
│ │ ├── static/
│ │ │ ├── sw-custom-header.js
│ │ │ ├── sw-default-header.js
│ │ │ └── sw-disable.js
│ │ └── sw/
│ │ ├── test-disable.mjs
│ │ ├── test-enable.mjs
│ │ └── test-isSupported.mjs
│ ├── workbox-precaching/
│ │ ├── integration/
│ │ │ ├── test-cleanup-outdated-caches.js
│ │ │ ├── test-precache-and-update.js
│ │ │ └── test-sw.js
│ │ ├── static/
│ │ │ ├── addToCacheList.html
│ │ │ ├── cleanup-outdated-caches/
│ │ │ │ ├── sw.js
│ │ │ │ └── test.txt
│ │ │ ├── precache-and-update/
│ │ │ │ ├── hashed-file.abcd1234.txt
│ │ │ │ ├── index.html
│ │ │ │ ├── styles/
│ │ │ │ │ └── index.css
│ │ │ │ ├── sw-1.js
│ │ │ │ └── sw-2.js
│ │ │ ├── precache.html
│ │ │ └── project/
│ │ │ ├── example-2.html
│ │ │ ├── example-a.html
│ │ │ ├── example-b.html
│ │ │ ├── example-timestamp.html
│ │ │ ├── example.html
│ │ │ └── index.html
│ │ └── sw/
│ │ ├── resetDefaultPrecacheController.mjs
│ │ ├── test-PrecacheController.mjs
│ │ ├── test-PrecacheFallbackPlugin.mjs
│ │ ├── test-PrecacheRoute.mjs
│ │ ├── test-PrecacheStrategy.mjs
│ │ ├── test-addPlugins.mjs
│ │ ├── test-addRoute.mjs
│ │ ├── test-cleanupOutdatedCaches.mjs
│ │ ├── test-createHandlerBoundToURL.mjs
│ │ ├── test-getCacheKeyForURL.mjs
│ │ ├── test-matchPrecache.mjs
│ │ ├── test-precache.mjs
│ │ ├── test-precacheAndRoute.mjs
│ │ └── utils/
│ │ ├── test-deleteOutdatedCaches.mjs
│ │ ├── test-printCleanupDetails.mjs
│ │ └── test-printInstallDetails.mjs
│ ├── workbox-range-requests/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── test-RangeRequestsPlugin.mjs
│ │ ├── test-createPartialResponse.mjs
│ │ └── utils/
│ │ ├── test-calculateEffectiveBoundaries.mjs
│ │ └── test-parseRangeHeader.mjs
│ ├── workbox-routing/
│ │ ├── integration/
│ │ │ ├── test-navigation-route.js
│ │ │ ├── test-routing-basic.js
│ │ │ ├── test-routing-regex.js
│ │ │ └── test-sw.js
│ │ ├── static/
│ │ │ ├── routing-basic/
│ │ │ │ ├── index.html
│ │ │ │ └── sw.js
│ │ │ ├── routing-navigation/
│ │ │ │ ├── index.html
│ │ │ │ └── sw.js
│ │ │ ├── routing-regex/
│ │ │ │ ├── index.html
│ │ │ │ └── sw.js
│ │ │ ├── routing.html
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── test-NavigationRoute.mjs
│ │ ├── test-RegExpRoute.mjs
│ │ ├── test-Route.mjs
│ │ ├── test-Router.mjs
│ │ ├── test-registerRoute.mjs
│ │ ├── test-setCatchHandler.mjs
│ │ ├── test-setDefaultHandler.mjs
│ │ └── utils/
│ │ └── test-normalizeHandler.mjs
│ ├── workbox-strategies/
│ │ ├── integration/
│ │ │ ├── test-cacheFirst.js
│ │ │ ├── test-cacheOnly.js
│ │ │ ├── test-networkFirst.js
│ │ │ ├── test-networkOnly.js
│ │ │ ├── test-staleWhileRevalidate.js
│ │ │ └── test-sw.js
│ │ ├── static/
│ │ │ ├── cache-first/
│ │ │ │ ├── example.txt
│ │ │ │ └── sw.js
│ │ │ ├── cache-only/
│ │ │ │ └── sw.js
│ │ │ ├── network-first/
│ │ │ │ └── sw.js
│ │ │ ├── network-only/
│ │ │ │ └── sw.js
│ │ │ └── stale-while-revalidate/
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── plugins/
│ │ │ └── test-cacheOkAndOpaquePlugin.mjs
│ │ ├── test-CacheFirst.mjs
│ │ ├── test-CacheOnly.mjs
│ │ ├── test-NetworkFirst.mjs
│ │ ├── test-NetworkOnly.mjs
│ │ ├── test-StaleWhileRevalidate.mjs
│ │ ├── test-Strategy.mjs
│ │ ├── test-StrategyHandler.mjs
│ │ └── test-UsageWithRouter.mjs
│ ├── workbox-streams/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ ├── 4.txt
│ │ │ ├── index.html
│ │ │ └── sw.js
│ │ └── sw/
│ │ ├── test-isSupported.mjs
│ │ └── utils/
│ │ └── test-createHeaders.mjs
│ ├── workbox-sw/
│ │ ├── integration/
│ │ │ └── test-all.js
│ │ ├── static/
│ │ │ ├── example.css
│ │ │ ├── example.js
│ │ │ ├── index.html
│ │ │ ├── integration/
│ │ │ │ ├── index.html
│ │ │ │ ├── invalid-sw.js
│ │ │ │ └── valid-sw.js
│ │ │ └── sw.js
│ │ └── sw/
│ │ └── controllers/
│ │ └── test-WorkboxSW.mjs
│ ├── workbox-webpack-plugin/
│ │ ├── node/
│ │ │ ├── dependency-check.js
│ │ │ ├── v4/
│ │ │ │ ├── generate-sw.js
│ │ │ │ ├── inject-manifest.js
│ │ │ │ └── lib/
│ │ │ │ └── create-webpack-asset-plugin.js
│ │ │ └── v5/
│ │ │ ├── generate-sw.js
│ │ │ ├── inject-manifest.js
│ │ │ └── lib/
│ │ │ └── create-webpack-asset-plugin.js
│ │ └── static/
│ │ ├── bad-multiple-injection.js
│ │ ├── example-project-1/
│ │ │ ├── index.html
│ │ │ ├── page-1.html
│ │ │ ├── page-2.html
│ │ │ ├── splitChunksEntry.js
│ │ │ ├── styles/
│ │ │ │ ├── stylesheet-1.css
│ │ │ │ └── stylesheet-2.css
│ │ │ └── webpackEntry.js
│ │ ├── injected-manifest.js
│ │ ├── injected-manifest.json
│ │ ├── module-import-sw.js
│ │ ├── sw-src-define-plugin.js
│ │ ├── sw-src-missing-sourcemap.js
│ │ ├── sw-src.js
│ │ ├── sw.ts
│ │ └── wasm-project/
│ │ ├── add.wasm
│ │ ├── index.js
│ │ └── worker.js
│ └── workbox-window/
│ ├── integration/
│ │ └── test-all.js
│ ├── static/
│ │ └── index.html
│ └── window/
│ ├── sw-error.js
│ ├── sw-message-reply.js
│ └── test-Workbox.mjs
├── tsconfig.json
└── typescript.eslintrc.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.js text eol=lf
*.mjs text eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
Welcome! Please use this template for reporting bugs or requesting features. For questions about using Workbox, the best place to ask is Stack Overflow, tagged with `[workbox]`: https://stackoverflow.com/questions/ask?tags=workbox
**Library Affected**:
_workbox-sw, workbox-build, etc._
**Browser & Platform**:
_E.g. Google Chrome v51.0.1 for Android, or "all browsers"._
**Issue or Feature Request Description**:
_Your request here._
_When reporting bugs, please include relevant JavaScript Console logs and links to public URLs at which the issue could be reproduced._
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
**Prior to creating a pull request, please follow all the steps in the [contributing guide](https://github.com/GoogleChrome/workbox/blob/v6/CONTRIBUTING.md).**
Fixes #_issue number_
_Description of what's changed/fixed._
================================================
FILE: .github/workflows/pull-request.yml
================================================
name: Test Suite
on: [pull_request]
jobs:
Node_Tests_Windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Get npm cache directory
id: npm-cache
run: |
echo "::set-output name=dir::$(npm config get cache)"
- uses: actions/cache@v4
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Setup
run: |
npm ci
npx gulp build
- run: npx gulp test_node
Full_Suite_Mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- uses: actions/cache@v4
with:
path: ~/.selenium-assistant
key: ${{ runner.os }}
- name: Setup
run: |
sudo safaridriver --enable
npm ci
npx gulp build
- run: npx gulp test
================================================
FILE: .github/workflows/scorecards-analysis.yml
================================================
name: Scorecards supply-chain security
on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '33 8 * * 4'
push:
branches: [v7]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecards analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Used to receive a badge. (Upcoming feature)
id-token: write
actions: read
contents: read
steps:
- name: 'Checkout code'
uses: actions/checkout@v5
with:
persist-credentials: false
- name: 'Run analysis'
uses: ossf/scorecard-action@v2.4.3
with:
results_file: results.sarif
results_format: sarif
# Read-only PAT token. To create it,
# follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
# Publish the results to enable scorecard badges. For more details, see
# https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories, `publish_results` will automatically be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: 'Upload artifact'
uses: actions/upload-artifact@v5
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
================================================
FILE: .gitignore
================================================
.DS_Store
build
lerna-debug.log
node_modules
npm-debug.log
packages/**/LICENSE
temp
tmp-*
!packages/workbox-cli/test/static/example-project-1/node_modules
coverage/
generated-release-files/
workbox-*.json
.esm-cache/
.nyc_output/
.rpt2_cache/
docs
local-builds/
firebase-debug.log
.firebase
# Generated TypeScript files and build data
tsconfig.tsbuildinfo
packages/workbox-*/**/*.d.ts
!packages/workbox-*/src/**/*.d.ts
packages/workbox-*/**/*.js
!packages/workbox-build/**/*.js
!packages/workbox-webpack-plugin/**/*.js
# Individual package ignores during TypeScript migration
packages/workbox-background-sync/**/*.mjs
packages/workbox-broadcast-update/**/*.mjs
packages/workbox-cacheable-response/**/*.mjs
packages/workbox-core/**/*.mjs
packages/workbox-expiration/**/*.mjs
packages/workbox-google-analytics/**/*.mjs
packages/workbox-navigation-preload/**/*.mjs
packages/workbox-precaching/**/*.mjs
packages/workbox-range-requests/**/*.mjs
packages/workbox-recipes/**/*.mjs
packages/workbox-routing/**/*.mjs
packages/workbox-strategies/**/*.mjs
packages/workbox-streams/**/*.mjs
packages/workbox-window/**/*.mjs
================================================
FILE: .husky/.gitignore
================================================
_
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
# Please see https://typicode.github.io/husky/#/?id=command-not-found
# if you have trouble running this command.
. "$(dirname "$0")/_/husky.sh"
npm run lint-staged
================================================
FILE: .husky/pre-push
================================================
#!/bin/sh
# Please see https://typicode.github.io/husky/#/?id=command-not-found
# if you have trouble running this command.
. "$(dirname "$0")/_/husky.sh"
npm run lint
================================================
FILE: .ncurc.js
================================================
// We use `npx npm-check-updates` to find updates to dependencies.
// Some dependencies have breaking changes that we can't resolve.
// This config file excludes those dependencies from the checks
// until we're able to remediate our code to deal with them.
module.exports = {
reject: [
// See https://github.com/GoogleChrome/workbox/issues/2479
'service-worker-mock',
],
};
================================================
FILE: .nvmrc
================================================
20
================================================
FILE: .prettierignore
================================================
_version.ts
.nyc_output
.vscode
*.hbs
*.log
build
coverage
docs
generated-release-files
node_modules
package-lock.json
packages/**/*.d.ts
packages/**/*.js
packages/**/*.mjs
packages/workbox-build/src/schema/*.json
temp
test/workbox-webpack-plugin/static/injected-manifest.json
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
All Google open source projects are covered by our [community guidelines](https://opensource.google/conduct/) which define the kind of respectful behavior we expect of all participants.
================================================
FILE: CONTRIBUTING.md
================================================
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
- If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual).
- If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate).
Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
1. The repo owner will respond to your issue promptly.
1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above).
1. Fork the repo, develop and test your code changes (see details below).
1. Ensure that your code adheres to the existing style in the sample to which you are contributing.
1. Submit a pull request.
## Setting up your environment
Workbox uses [`node`](https://nodejs.org/) and its related toolchain (`npm`, etc.) to install dependencies and run the build and test processes. Please ensure that you have a working `node` installation before proceeding.
Workbox uses `git` hooks via [`husky`](https://typicode.github.io/husky/#/) to automatically run code formatters and linters when committing and pushing code to GitHub. If you're running into issues with the `git` hooks, you may need to [create a `~/.huskyrc` file](https://typicode.github.io/husky/#/?id=command-not-found) to set up your `$PATH` correctly.
It's expected that the Workbox development environment should work on Windows, macOS, and Linux. If you encounter any platform-specific issues, please [open a bug](https://github.com/GoogleChrome/workbox/issues/new).
## Testing your contribution
When making local changes, you'll probably want to ensure that your code builds and passes our test suite. To do this, run the following in your local clone of the repo:
```sh
$ npm ci
$ npm run gulp build
$ npm run gulp test
```
Note that on Windows, `npm run gulp test` will only run a subset of our test suite. The full test suite will always be run as part of the GitHub continuous integration environment against your pull request.
When you add a new feature or fix a bug, please check the test suite to see if its appropriate to add or modify an existing test to cover the updated functionality.
## Running a subset of the tests
Workbox's test suite is split in two parts: one for the `node`-based tooling (`workbox-cli`, `workbox-build`, `workbox-webpack-plugin`) and one for the browser-based code.
To run the tests for just the `node`-based tooling:
```sh
npm run gulp test_node
```
To interactively run tests for the browser-based code, launch the test server:
```sh
npm run gulp test_server
```
Then open a web browser to http://localhost:3004/ and navigate to the test suite for the package you're interested in. For example, to run the tests for `workbox-strategies`, go to http://localhost:3004/test/workbox-streams/sw/
To do an automated run of the browser-based test suite against the full set of supported browsers, run:
```sh
npm run gulp test_integration
```
================================================
FILE: LICENSE
================================================
Copyright 2018 Google LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Welcome to Workbox!
Workbox is a collection of JavaScript libraries for
[Progressive Web Apps](https://web.dev/progressive-web-apps/).
## Documentation
- [Overview](https://developer.chrome.com/docs/workbox/)
- [Get started](https://developer.chrome.com/docs/workbox/what-is-workbox)
- [Contribute](CONTRIBUTING.md)
## Maintenance update
Workbox is a powerful library originally developed by members of Chrome's
developer relations team to facilitate the creation of Progressive Web Apps
and to improve the offline experience of web applications. It offers a suite
of tools and strategies for efficiently caching and serving web assets,
managing service workers, and handling offline scenarios. Workbox simplifies
the implementation of common caching patterns and provides developers with a
comprehensive toolkit to build robust, resilient web applications. From now
on, [Chrome's Aurora team](https://developer.chrome.com/docs/aurora) will be
the new owners of Workbox.
## Contributing
Development happens in the open on GitHub. We're thankful to the community for
contributing any improvements.
Please read the [guide to contributing](CONTRIBUTING.md) for information about
setting up your environment and other requirements prior to filing any
pull requests.
## License
MIT. See [LICENSE](LICENSE) for details.
================================================
FILE: cdn-details.json
================================================
{
"origin": "https://storage.googleapis.com",
"bucketName": "workbox-cdn",
"releasesDir": "releases"
}
================================================
FILE: demos/README.md
================================================
# workbox-module-demos
Contains sample demos that are deployed to Glitch at https://glitch.com/@philkrie/workbox-demos and used as examples at https://developers.google.com/web/tools/workbox/modules
================================================
FILE: demos/src/workbox-background-sync-demo/index.html
================================================
workbox-background-sync demo
Open DevTools.
Switch to the Network panel.
Toggle `Disable cache` checkbox on (it's unchecked by default).
Click `Make Fetch request` button below.
Observe that request is successfully completed with status 200.
Disconnect from actual network (DevTools offline doesn't work for
background sync).
Again, click `Make Fetch request` button below.
Observe that request is aborted due to no network.
Connect to your network and observe the Network panel in DevTools.
Once the network is connected, you will see that the request is retried
automatically upon the `sync` event.
Make fetch request
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-background-sync-demo/package.json
================================================
{
"name": "workbox-background-sync",
"version": "1.0.0",
"description": "Workbox Background Sync Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-background-sync-demo/public/example.txt
================================================
example text
================================================
FILE: demos/src/workbox-background-sync-demo/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: true,
});
const bgSyncPlugin = new workbox.backgroundSync.BackgroundSyncPlugin(
'myQueueName',
);
workbox.routing.registerRoute(
({url}) => url.pathname === '/example.txt',
new workbox.strategies.NetworkOnly({
plugins: [bgSyncPlugin],
}),
);
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-background-sync-demo/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-broadcast-update-demo/index.html
================================================
workbox-broadcast-update demo
Open DevTools
Go to the Console
Click any of the buttons below and view the logs:
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-broadcast-update-demo/package.json
================================================
{
"name": "workbox-broadcast-update",
"version": "1.0.0",
"description": "Workbox Broadcast Update Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-broadcast-update-demo/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
const broadcastUpdate = new workbox.broadcastUpdate.BroadcastCacheUpdate(
'broadcast-update-demo',
);
const initialResponse = new Response('Response 1', {
headers: {
'last-modified': Date.now().toString(),
},
});
self.addEventListener('message', (event) => {
switch (event.data.command) {
case 'trigger-broadcast': {
const secondResponse = new Response('Response 2', {
headers: {
'last-modified': Date.now().toString(),
},
});
broadcastUpdate.notifyIfUpdated({
oldResponse: initialResponse,
newResponse: secondResponse,
request: new Request('exampleUrl'),
url: 'exampleUrl',
cacheName: 'exampleCacheName',
});
break;
}
default:
console.log(`Unknown command received in the service worker: `, event);
}
});
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-broadcast-update-demo/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-cacheable-response/index.html
================================================
workbox-cacheable-response demo
Open DevTools
Go to the Console
Click any of the buttons below and view the logs and network responses:
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-cacheable-response/package.json
================================================
{
"name": "workbox-cacheable-response",
"version": "1.0.0",
"description": "Workbox Cacheable Response Demo Listener",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-cacheable-response/server.js
================================================
// server.js
// where your node app starts
// init project
const express = require('express');
const app = express();
// we've started you off with Express,
// but feel free to use whatever libs or frameworks you'd like through `package.json`.
// http://expressjs.com/en/starter/basic-routing.html
app.get('/', function (request, response) {
response.sendFile(__dirname + '/index.html');
});
app.get('/sw.js', function (request, response) {
response.sendFile(__dirname + '/sw.js');
});
// listen for requests :)
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
app.get('/api/is-response-cacheable', function (req, res) {
if (req.headers['x-is-cacheable']) {
const value = req.headers['x-is-cacheable'];
res.set('X-Is-Cacheable', value);
res.send(`This response has 'X-Is-Cacheable' header set to '${value}'`);
} else {
res.send(`This response has no 'X-Is-Cacheable' header`);
}
});
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
================================================
FILE: demos/src/workbox-cacheable-response/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
const cacheable = new workbox.cacheableResponse.CacheableResponse({
statuses: [200],
headers: {
'X-Is-Cacheable': 'true',
},
});
const handleCachableResponse = (event) => {
return fetch(event.request).then((response) => {
if (cacheable.isResponseCacheable(response)) {
console.log('Response meets the criteria');
} else {
console.log('Response does NOT meet the criteria');
}
return response;
});
};
self.addEventListener('fetch', (event) => {
switch (new URL(event.request.url).pathname) {
case '/api/is-response-cacheable':
event.respondWith(handleCachableResponse(event));
break;
}
});
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-core/index.html
================================================
workbox-core demo
Open DevTools
Go to the Console
Click this button:
Install Service Worker
Checkout the logs for info on Workbox and getting started.
Click this button to print out various types of logs:
Show Me Workbox Logs
Click this button to print out the Workbox Cache Names:
Show Me Workbox Cache Names
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-core/package.json
================================================
{
"name": "workbox-core",
"version": "1.0.0",
"description": "Workbox Core Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-core/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: true,
});
// To avoid async issues, we load core before we call it in the callback
workbox.loadModule('workbox-core');
const printLogs = () => {
// ☠️ You should never so this - this is just so we can show off our logging.
workbox.core._private.logger.debug(`🐛 Oh hai! I'm a debug message.`);
workbox.core._private.logger.log(`🔧 Good ole log message.`);
workbox.core._private.logger.warn(`⚠️ Uh Oh.... I'm a warning.`);
workbox.core._private.logger.error(`☠️ Stuff is breaking. I'm an error.`);
};
const printCacheNames = () => {
workbox.core._private.logger.log(`The caches used by Workbox are...`);
const cacheNames = workbox.core.cacheNames;
Object.keys(cacheNames).forEach((cacheId) => {
console.log(` ${cacheId}: ${cacheNames[cacheId]}`);
});
};
self.addEventListener('message', (event) => {
switch (event.data.command) {
case 'printLogs':
printLogs();
break;
case 'printCacheNames':
printCacheNames();
break;
default:
console.log(`Unknown command received in the service worker: `, event);
}
});
================================================
FILE: demos/src/workbox-core/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
///////////////////////////////////////////////////////////////////////////// */
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-expiration/index.html
================================================
workbox-expiration demo
Open DevTools
Go to the Console
Click the following buttons and look at the Cache and IndexedDB entries.
Entry 1 (Expires in:
Not Added Yet )
Entry 2 (Expires in:
Not Added Yet )
Entry 3 (Expires in:
Not Added Yet )
Entry 4 (Expires in:
Not Added Yet )
Entry 5 (Expires in:
Not Added Yet )
To remove any expired entries click here:
Expire Entries
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-expiration/package.json
================================================
{
"name": "workbox-expiration",
"version": "1.0.0",
"description": "Workbox Expiration Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-expiration/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: true,
});
const expirationManager = new workbox.expiration.CacheExpiration(
'demo-cache-for-expiration',
{
maxEntries: 3,
maxAgeSeconds: 30,
},
);
const updateEntry = async (entryID) => {
const openCache = await caches.open('demo-cache-for-expiration');
openCache.put(
`example-entry-${entryID}`,
new Response(`Hello from entry number ${entryID}`),
);
expirationManager.updateTimestamp(`example-entry-${entryID}`);
};
self.addEventListener('message', (event) => {
switch (event.data.command) {
case 'update-entry':
updateEntry(event.data.id);
break;
case 'expire-entries':
expirationManager.expireEntries();
break;
default:
console.log(`Unknown command received in the service worker: `, event);
}
});
================================================
FILE: demos/src/workbox-expiration/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-google-analytics/index.html
================================================
workbox-google-analytics demo
Open DevTools
Go to the Console
Click this button:
Make Analytics Call
Turn off your machine's connection and press the button again. Of
course, the requests fail
Upon reconnecting to the network, workbox-googleAnalytics will empty the
request queue
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-google-analytics/package.json
================================================
{
"name": "workbox-google-analytics",
"version": "1.0.0",
"description": "Workbox Google Analytics Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-google-analytics/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
workbox.googleAnalytics.initialize();
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-google-analytics/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-navigation-preload/index.html
================================================
workbox-navigation-preload demo
workbox-navigation-preload enables navigation preload in browsers that
support it
Open the dev tools console and reload this page
There should be two requests for the HTML document: one initiated by the
service worker, and one by Preload
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-navigation-preload/package.json
================================================
{
"name": "workbox-navigation-preload",
"version": "1.0.0",
"description": "Workbox Navigation Preload Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-navigation-preload/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
// Enable navigation preload.
workbox.navigationPreload.enable();
// Swap in NetworkOnly, CacheFirst, or StaleWhileRevalidate as needed.
const strategy = new workbox.strategies.NetworkFirst({
cacheName: 'cached-navigations',
plugins: [
// Any plugins, like workbox.expiration, etc.
],
});
const navigationRoute = new workbox.routing.NavigationRoute(strategy, {
// Optionally, provide a allowlist/denylist of RegExps to determine
// which paths will match this route.
// allowlist: [],
// denylist: [],
});
workbox.routing.registerRoute(navigationRoute);
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-navigation-preload/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-precaching/index.html
================================================
workbox-precaching demo
Open DevTools
Go to the Console
Click this button:
Install Service Worker
Checkout the logs for info on what was precached.
Explore Cache in the "Application" tab in DevTools.
Note: You may need to right-click and refresh.
Click this button:
Install New Service Worker
Check logs, cache to highlight the new, updated and deleted entry.
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-precaching/package.json
================================================
{
"name": "workbox-precaching",
"version": "1.0.0",
"description": "Workbox Precaching Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-precaching/public/hello-world.1234.txt
================================================
Hello, World!
I'm the first version of this file.
================================================
FILE: demos/src/workbox-precaching/public/hello-world.5678.txt
================================================
Hello, World!
I'm the second version of this file.
================================================
FILE: demos/src/workbox-precaching/public/test-file.txt
================================================
#Danger
This file is not revisioned.
================================================
FILE: demos/src/workbox-precaching/sw-1.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
workbox.precaching.precacheAndRoute([
{url: '/', revision: '1'},
{url: 'test-file.txt', revision: '1'},
'hello-world.1234.txt',
]);
================================================
FILE: demos/src/workbox-precaching/sw-2.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
workbox.precaching.precacheAndRoute([
{url: '/', revision: '2'},
{url: 'test-file.txt', revision: '2'},
'hello-world.5678.txt',
]);
================================================
FILE: demos/src/workbox-precaching/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw-1.js', (request, response) => {
response.sendFile(path.resolve('sw-1.js'));
});
app.get('/sw-2.js', (request, response) => {
response.sendFile(path.resolve('sw-2.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-range-requests/index.html
================================================
workbox-range-requests demo
Open DevTools
Go to the Console
Click this button to request range 1 - 4 of 'hello, world.':
Make Range Request
Checkout the logs for info on the range request that was handled.
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-range-requests/package.json
================================================
{
"name": "workbox-range-requests",
"version": "1.0.0",
"description": "Workbox Range Requests Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-range-requests/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
workbox.routing.registerRoute(
new RegExp('/range-request-example'),
new workbox.strategies.CacheOnly({
cacheName: 'range-requests-demo',
plugins: [new workbox.rangeRequests.RangeRequestsPlugin()],
}),
);
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-range-requests/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-routing/index.html
================================================
workbox-precaching demo
Open DevTools
Go to the Console
Click this button:
Install Service Worker
Refresh the page
Notice that the image '/public/demo-img.png' was routed and is now a
party popper
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-routing/package.json
================================================
{
"name": "workbox-routing",
"version": "1.0.0",
"description": "Workbox Routing Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-routing/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
// Set logs level to `debug` to view all logs
// workbox.core.setLogLevel(workbox.core.LOG_LEVELS.debug);
// Set up a route to alter the demo-img
const matchCb = ({url, event}) => {
return url.pathname === '/demo-img.png';
};
const handlerCb = ({url, event, params}) => {
return fetch('/demo-popper.png');
};
// First parameter can be a string, RegExp, workbox Route, or a match callback (used here)
// Second parameter handles the response and must return a Response promise
workbox.routing.registerRoute(matchCb, handlerCb);
const GLITCH_ICO_URL = 'https://glitch.com/edit/favicon-app.ico';
workbox.routing.registerRoute(GLITCH_ICO_URL, () => {
// Demonstrating when an error is thrown by a Route.
throw new Error(`Example error thrown from the default handler`);
});
workbox.routing.setCatchHandler(({event}) => {
return fetch(event.request);
});
================================================
FILE: demos/src/workbox-routing/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
///////////////////////////////////////////////////////////////////////////// */
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-strategies/index.html
================================================
workbox-strategies demo
Open DevTools
Go to the Console
Click any of the buttons below and view the logs:
This will attempt to get a request from an empty cache.
Cache Only Request (To Empty Cache)
Cache Only Request (To Populated Cache)
Cache First Request (Try it Several Times)
Network Only Request
Network First Request (Valid URL)
Network First Request (Failing URL)
Stale While Revalidate Request
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-strategies/public/cache-first.txt
================================================
Hello from CacheFirst
================================================
FILE: demos/src/workbox-strategies/public/network-first.txt
================================================
Hello from NetworkFirst
================================================
FILE: demos/src/workbox-strategies/public/network-only.txt
================================================
Hello from NetworkOnly
================================================
FILE: demos/src/workbox-strategies/public/stale-while-revalidate.txt
================================================
Hello from StaleWhileRevalidate
================================================
FILE: demos/src/workbox-strategies/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
// To avoid async issues, we load strategies before we call it in the event listener
workbox.loadModule('workbox-strategies');
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: true,
});
self.addEventListener('fetch', (event) => {
const request = event.request;
switch (new URL(event.request.url).pathname) {
case '/cache-only-empty-cache.txt': {
const cacheOnlyEmpty = new workbox.strategies.CacheOnly();
event.respondWith(cacheOnlyEmpty.handle({event, request}));
break;
}
case '/cache-only-populated-cache': {
const cacheOnlyPopulated = new workbox.strategies.CacheOnly();
event.respondWith(cacheOnlyPopulated.handle({event, request}));
break;
}
case '/cache-first.txt': {
const cacheFirst = new workbox.strategies.CacheFirst();
event.respondWith(cacheFirst.handle({event, request}));
break;
}
case '/network-only.txt': {
const networkOnly = new workbox.strategies.NetworkOnly();
event.respondWith(networkOnly.handle({event, request}));
break;
}
case '/network-first.txt': {
const networkFirst = new workbox.strategies.NetworkFirst();
event.respondWith(networkFirst.handle({event, request}));
break;
}
case '/network-first-404.txt': {
const networkFirstInvalid = new workbox.strategies.NetworkFirst();
event.respondWith(networkFirstInvalid.handle({event, request}));
break;
}
case '/stale-while-revalidate.txt': {
const staleWhileRevalidate =
new workbox.strategies.StaleWhileRevalidate();
event.respondWith(staleWhileRevalidate.handle({event, request}));
break;
}
}
});
// This immediately deploys the service worker w/o requiring a refresh
workbox.core.skipWaiting();
workbox.core.clientsClaim();
// Populate the cache to illustrate cache-only-populated-cache route
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(workbox.core.cacheNames.runtime).then((cache) => {
return cache.put(
new Request('/cache-only-populated-cache'),
new Response('Hello from the populated cache.'),
);
}),
);
});
================================================
FILE: demos/src/workbox-strategies/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw1.js', (request, response) => {
response.sendFile(path.resolve('sw1.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-streams/index.html
================================================
workbox-streams demo
Open DevTools
Go to the Console
Click this button to add an iframe to this page, whose body
is created from multiple streamed sources:
Add iframe
Checkout the logs for info on how the streamed response was generated.
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-streams/package.json
================================================
{
"name": "workbox-streams",
"version": "1.0.0",
"description": "Workbox Streams Demo Git Listener",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-streams/server.js
================================================
// server.js
// where your node app starts
// init project
const express = require('express');
const app = express();
// we've started you off with Express,
// but feel free to use whatever libs or frameworks you'd like through `package.json`.
// http://expressjs.com/en/starter/basic-routing.html
app.get('/', function (request, response) {
response.sendFile(__dirname + '/index.html');
});
app.get('/sw.js', function (request, response) {
response.sendFile(__dirname + '/sw.js');
});
// listen for requests :)
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
app.get('/api/date', function (req, res) {
res.header('Content-Type', 'text/plain');
res.header('Cache-Control', 'no-cache');
res.send(`Received from the server at ${new Date().toLocaleString()}`);
});
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
================================================
FILE: demos/src/workbox-streams/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
const CACHE_NAME = 'my-cache';
const START_CACHE_KEY = 'start';
const END_CACHE_KEY = 'end';
self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
await Promise.all([
cache.put(START_CACHE_KEY, new Response('')),
cache.put(END_CACHE_KEY, new Response('')),
]);
})(),
);
});
// Use a stale-while-revalidate strategy as a source for part of the response.
const apiStrategy = new workbox.strategies.StaleWhileRevalidate({
cacheName: 'apiStrategy',
});
// String together an artificially complex series of stream sources.
const streamsStrategy = workbox.streams.strategy([
() => caches.match(START_CACHE_KEY, {cacheName: CACHE_NAME}),
() => `🎉 This iframe is composed of multiple streams.
`,
() => `Here's an API call, using a stale-while-revalidate strategy:
`,
({event}) =>
apiStrategy.handle({
event: event,
request: new Request('/api/date'),
}),
() => caches.match(END_CACHE_KEY, {cacheName: CACHE_NAME}),
]);
// Once the strategy is configured, the actual routing looks clean.
workbox.routing.registerRoute(new RegExp('iframe$'), streamsStrategy);
workbox.core.skipWaiting();
workbox.core.clientsClaim();
================================================
FILE: demos/src/workbox-sw/README.md
================================================
# workbox-sw-demo
================================================
FILE: demos/src/workbox-sw/index.html
================================================
workbox-sw demo
Open DevTools and go to the console
Click this button:
Install Service Worker
You should see a workbox message appear
Refresh the page, go to the network tab, and check the "Size" of
demo-img.png. It should read (ServiceWorker)
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-sw/package.json
================================================
{
"name": "workbox-sw",
"version": "1.0.0",
"description": "Workbox SW Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-sw/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: true,
});
workbox.precaching.precacheAndRoute([
'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css',
]);
// Demonstrates using default cache
workbox.routing.registerRoute(
new RegExp('.*\\.(?:js)'),
new workbox.strategies.NetworkFirst(),
);
// Demonstrates a custom cache name for a route.
workbox.routing.registerRoute(
new RegExp('.*\\.(?:png|jpg|jpeg|svg|gif)'),
new workbox.strategies.CacheFirst({
cacheName: 'image-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 3,
}),
],
}),
);
================================================
FILE: demos/src/workbox-sw/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: demos/src/workbox-window/index.html
================================================
workbox-window demo
workbox-window runs in the window context, not the service worker
Open up DevTools and go to the console
Back to Demos
Docs |
GitHub |
@workboxjs
================================================
FILE: demos/src/workbox-window/package.json
================================================
{
"name": "workbox-window",
"version": "1.0.0",
"description": "Workbox Window Demo Git Listener",
"scripts": {
"start": "node updateServer.js"
},
"dependencies": {
"express": "^4.17.1"
},
"engines": {
"node": "8.x"
},
"license": "MIT"
}
================================================
FILE: demos/src/workbox-window/sw.js
================================================
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js',
);
workbox.setConfig({
debug: true,
});
const SW_VERSION = '5.0.0';
addEventListener('message', (event) => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
================================================
FILE: demos/src/workbox-window/updateServer.js
================================================
const express = require('express');
const app = express();
const path = require('path');
app.get('/', (request, response) => {
response.sendFile(path.resolve('index.html'));
});
app.get('/sw.js', (request, response) => {
response.sendFile(path.resolve('sw.js'));
});
app.use(express.static('public'));
/* /////////////////////////////////////////////////////////////////////////////
The code below this comment is unrelated to the demo and used for maintenance
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
/////////////////////////////////////////////////////////////////////////////*/
const {execSync} = require('child_process');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.post('/deploy', (request, response) => {
if (request.query.secret !== process.env.SECRET) {
response.status(401).send();
return;
}
const repoUrl = request.query.repo;
execSync(
`git checkout -- ./ && git pull -X theirs ${repoUrl} ` +
`glitch && refresh && git branch -D glitch`,
);
response.status(200).send();
});
const listener = app.listen(process.env.PORT, function () {
console.log('Your app is listening on port ' + listener.address().port);
});
================================================
FILE: gulp-tasks/analyze-properties.js
================================================
/*
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.
*/
const AnalyseBuildForProperties = require('./utils/analyse-properties');
async function analyze_properties() {
const analysisTool = new AnalyseBuildForProperties();
const results = await analysisTool.run();
for (const result of results) {
analysisTool.printDetails(result);
}
}
module.exports = {
analyze_properties,
};
================================================
FILE: gulp-tasks/build-node-packages.js
================================================
/*
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.
*/
const {parallel} = require('gulp');
const execa = require('execa');
const fse = require('fs-extra');
const TJS = require('typescript-json-schema');
const upath = require('upath');
const constants = require('./utils/constants');
const packageRunner = require('./utils/package-runner');
async function buildNodePackage(packagePath) {
const outputDirectory = upath.join(
packagePath,
constants.PACKAGE_BUILD_DIRNAME,
);
const configFile = upath.join(
__dirname,
'utils',
'node-projects-babel.config.json',
);
await execa(
'babel',
[
'--config-file',
configFile,
`${packagePath}/src`,
'--out-dir',
outputDirectory,
'--copy-files',
],
{preferLocal: true},
);
}
async function generateWorkboxBuildJSONSchema(packagePath) {
// We only want to do this for workbox-build, but this function might be
// run for any package, so exit early.
if (!packagePath.endsWith('workbox-build')) {
return;
}
const program = TJS.programFromConfig(
upath.join(packagePath, 'tsconfig.json'),
);
const generator = TJS.buildGenerator(program, {
noExtraProps: true,
required: true,
});
const optionTypes = [
'GenerateSWOptions',
'GetManifestOptions',
'InjectManifestOptions',
'WebpackGenerateSWOptions',
'WebpackInjectManifestOptions',
];
for (const optionType of optionTypes) {
const schema = generator.getSchemaForSymbol(optionType);
if (schema.properties.manifestTransforms) {
schema.properties.manifestTransforms.items = {};
}
if (schema.properties.exclude) {
schema.properties.exclude.items = {};
}
if (schema.properties.include) {
schema.properties.include.items = {};
}
// See https://github.com/GoogleChrome/workbox/issues/2910
if (schema.definitions.OnSyncCallback) {
schema.definitions.OnSyncCallback = {};
}
if (schema.definitions.AbortSignal) {
schema.definitions.AbortSignal = {};
}
if (schema.definitions.RouteMatchCallback) {
schema.definitions.RouteMatchCallback = {};
}
if (schema.definitions.RouteHandlerCallback) {
schema.definitions.RouteHandlerCallback = {};
}
// See https://github.com/GoogleChrome/workbox/issues/2901
if (schema.definitions.WorkboxPlugin) {
for (const plugin of Object.keys(
schema.definitions.WorkboxPlugin.properties,
)) {
schema.definitions.WorkboxPlugin.properties[plugin] = {};
}
}
await fse.writeJSON(
upath.join(packagePath, 'src', 'schema', `${optionType}.json`),
schema,
{spaces: 2},
);
}
}
module.exports = {
build_node_packages: parallel(
packageRunner('build_node_packages', 'node', buildNodePackage),
),
build_node_ts_packages: parallel(
packageRunner(
'build_node_ts_packages',
'node_ts',
generateWorkboxBuildJSONSchema,
),
),
};
================================================
FILE: gulp-tasks/build-packages.js
================================================
/*
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.
*/
const {parallel, series} = require('gulp');
const del = require('del');
const fse = require('fs-extra');
const upath = require('upath');
const {
build_node_packages,
build_node_ts_packages,
} = require('./build-node-packages');
const {build_sw_packages} = require('./build-sw-packages');
const {build_window_packages} = require('./build-window-packages');
const {transpile_typescript} = require('./transpile-typescript');
const constants = require('./utils/constants');
const packageRunner = require('./utils/package-runner');
async function cleanPackage(packagePath) {
// Delete generated files from the the TypeScript transpile.
if (await fse.pathExists(upath.join(packagePath, 'src', 'index.ts'))) {
// Store the list of deleted files, so we can delete directories after.
const deletedPaths = await del([
`${packagePath}/**/*.+(js|mjs|d.ts)`,
// Don't delete files in node_modules.
'!**/node_modules/**/*',
// Don't delete anything under src.
`!${packagePath}/src/**/*`,
]);
// Any directories in `deletedPaths` that are top-level directories to the
// package should also be deleted since those directories should only
// contain generated `.mjs` and `.d.ts` files.
const directoriesToDelete = new Set();
for (const deletedPath of deletedPaths) {
const relativePath = upath.relative(packagePath, deletedPath);
const directory = relativePath.split(upath.sep)[0];
directoriesToDelete.add(upath.join(packagePath, directory));
}
await del([...directoriesToDelete]);
}
// Delete build files.
await del(upath.join(packagePath, constants.PACKAGE_BUILD_DIRNAME));
// Delete tsc artifacts (if present).
await del(upath.join(packagePath, 'tsconfig.tsbuildinfo'));
}
// Wrap this in a function since it's used multiple times.
function cleanSequence() {
return parallel(packageRunner('build_packages_clean', 'all', cleanPackage));
}
module.exports = {
build_packages_clean: cleanSequence(),
build_packages: series(
// This needs to be a series, not in parallel, so that there isn't a
// race condition with the terser nameCache.
transpile_typescript,
series(build_sw_packages, build_window_packages),
parallel(build_node_packages, build_node_ts_packages),
),
};
================================================
FILE: gulp-tasks/build-sw-packages.js
================================================
/*
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.
*/
const {parallel, series} = require('gulp');
const {rollup} = require('rollup');
const fse = require('fs-extra');
const ol = require('common-tags').oneLine;
const upath = require('upath');
const constants = require('./utils/constants');
const logHelper = require('../infra/utils/log-helper');
const packageRunner = require('./utils/package-runner');
const pkgPathToName = require('./utils/pkg-path-to-name');
const rollupHelper = require('./utils/rollup-helper');
const versionModule = require('./utils/version-module');
// This makes Rollup assume workbox-* will be added to the global
// scope and replace references with the core namespace.
function globals(moduleId) {
if (moduleId === 'workbox') {
return moduleId;
}
const splitImportPath = moduleId.split('/');
if (splitImportPath[0].indexOf('workbox-') !== 0) {
throw new Error(`Unknown global module ID: ${moduleId}`);
}
const packageName = splitImportPath.shift();
const packagePath = upath.join(__dirname, '..', 'packages', packageName);
const namespacePathParts = splitImportPath.map((importPathPiece) => {
// The browser namespace will need the file extension removed
return upath.basename(importPathPiece, upath.extname(importPathPiece));
});
if (namespacePathParts.length === 0) {
// Tried to pull in default export of module - this isn't allowed.
// A specific file must be referenced
throw new Error(ol`You cannot use a module directly. Instead, you must
specify a named import, to facilitate tree-shaking. Please fix the
import: '${moduleId}'`);
}
let additionalNamespace;
if (namespacePathParts.length > 1) {
if (namespacePathParts[0] !== '_private' || namespacePathParts.length > 2) {
// Tried to pull in default export of module - this isn't allowed.
// A specific file must be referenced
throw new Error(ol`You cannot use nested files. It must be a top level
(and public) file or a file under '_private' in a module. Please fix
the import: '${moduleId}'`);
}
additionalNamespace = namespacePathParts[0];
}
// Get a package's browserNamespace so we know where it will be
// on the global scope (i.e. workbox.)
try {
const pkg = require(upath.join(packagePath, 'package.json'));
return [pkg.workbox.browserNamespace, additionalNamespace]
.filter((value) => value && value.length > 0)
.join('.');
} catch (err) {
logHelper.error(`Unable to get browserNamespace for '${packageName}'`);
throw err;
}
}
// This ensures all workbox-* modules are treated as external and are
// referenced as globals.
function externalAndPure(importPath) {
return importPath.indexOf('workbox-') === 0;
}
async function buildSWBundle(packagePath, buildType) {
const packageName = pkgPathToName(packagePath);
const packageIndex = upath.join(packagePath, 'index.mjs');
// First check if the bundle file exists, if it doesn't
// there is nothing to build
if (!(await fse.exists(packageIndex))) {
throw new Error(`Could not find ${packageIndex}`);
}
const pkgJson = require(upath.join(packagePath, 'package.json'));
if (!pkgJson.workbox || !pkgJson.workbox.browserNamespace) {
throw new Error(ol`You must define a 'workbox.browserNamespace' property in
${packageName}/package.json`);
}
let outputFilename = pkgJson.workbox.outputFilename || packageName;
if (pkgJson.workbox.prodOnly) {
// Bail out early if this is a non-prod build.
if (buildType !== constants.BUILD_TYPES.prod) {
return Promise.resolve();
}
} else {
// Prod-only builds (above) don't need the build type, but when there's a
// dev and prod build we have to include it.
outputFilename += `.${buildType.slice(0, 4)}`;
}
outputFilename += '.js';
const namespace = pkgJson.workbox.browserNamespace;
const outputDirectory = upath.join(
packagePath,
constants.PACKAGE_BUILD_DIRNAME,
);
const plugins = rollupHelper.getDefaultPlugins(buildType);
const bundle = await rollup({
input: packageIndex,
external: externalAndPure,
treeshake: {
moduleSideEffects: externalAndPure ? 'no-external' : false,
},
plugins,
onwarn: (warning) => {
if (
buildType === constants.BUILD_TYPES.prod &&
warning.code === 'UNUSED_EXTERNAL_IMPORT'
) {
// This can occur when using rollup-plugin-replace.
logHelper.warn(`[${warning.code}] ${warning.message}`);
return;
}
// The final builds should have no warnings.
if (warning.code && warning.message) {
throw new Error(
`Unhandled Rollup Warning: [${warning.code}] ` + `${warning.message}`,
);
} else {
throw new Error(`Unhandled Rollup Warning: ${warning}`);
}
},
});
await bundle.write({
file: upath.join(outputDirectory, outputFilename),
name: namespace,
sourcemap: true,
format: 'iife',
globals,
esModule: false,
});
}
// This reads a little cleaner with a function to generate the sub-sequences.
function swBundleSequence() {
const builds = Object.keys(constants.BUILD_TYPES).map((type) =>
packageRunner(
'build_sw_packages_bundle',
'sw',
buildSWBundle,
constants.BUILD_TYPES[type],
),
);
return series(builds);
}
module.exports = {
build_sw_packages: series(
parallel(
packageRunner('build_sw_packages_version_module', 'sw', versionModule),
),
swBundleSequence(),
),
};
================================================
FILE: gulp-tasks/build-window-packages.js
================================================
/*
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.
*/
const {parallel, series} = require('gulp');
const {rollup} = require('rollup');
const fse = require('fs-extra');
const ol = require('common-tags').oneLine;
const upath = require('upath');
const constants = require('./utils/constants');
const logHelper = require('../infra/utils/log-helper');
const packageRunner = require('./utils/package-runner');
const pkgPathToName = require('./utils/pkg-path-to-name');
const rollupHelper = require('./utils/rollup-helper');
const versionModule = require('./utils/version-module');
async function buildWindowBundle(packagePath, buildType) {
const packageName = pkgPathToName(packagePath);
const packageIndex = upath.join(packagePath, 'index.mjs');
if (!(await fse.exists(packageIndex))) {
throw new Error(`Could not find ${packageIndex}`);
}
const outputDirectory = upath.join(
packagePath,
constants.PACKAGE_BUILD_DIRNAME,
);
const esmFilename = `${packageName}.${buildType.slice(0, 4)}.mjs`;
const esmLegacyFilename = `${packageName}.${buildType.slice(0, 4)}.es5.mjs`;
const umdFilename = `${packageName}.${buildType.slice(0, 4)}.umd.js`;
const onwarn = (warning) => {
// This can occur when using rollup-plugin-replace.
if (
buildType === constants.BUILD_TYPES.prod &&
warning.code === 'UNUSED_EXTERNAL_IMPORT'
) {
logHelper.warn(`[${warning.code}] ${warning.message}`);
return;
}
// The final builds should have no warnings.
if (warning.code && warning.message) {
throw new Error(ol`Unhandled Rollup Warning:
[${warning.code}] ${warning.message}`);
} else {
throw new Error(`Unhandled Rollup Warning: ${warning}`);
}
};
const mjsBundle = await rollup({
input: packageIndex,
plugins: rollupHelper.getDefaultPlugins(buildType, 'esm', false),
onwarn,
});
const es5Bundle = await rollup({
input: packageIndex,
plugins: rollupHelper.getDefaultPlugins(buildType, 'esm', true),
onwarn,
});
const umdBundle = await rollup({
input: packageIndex,
plugins: rollupHelper.getDefaultPlugins(buildType, 'umd', true),
onwarn,
});
// Generate both a native module and a UMD module (for compat).
await Promise.all([
mjsBundle.write({
file: upath.join(outputDirectory, esmFilename),
sourcemap: true,
format: 'esm',
}),
es5Bundle.write({
file: upath.join(outputDirectory, esmLegacyFilename),
sourcemap: true,
format: 'esm',
}),
umdBundle.write({
file: upath.join(outputDirectory, umdFilename),
sourcemap: true,
format: 'umd',
name: 'workbox',
}),
]);
}
// This reads a little cleaner with a function to generate the sub-sequences.
function windowBundleSequence() {
const builds = Object.keys(constants.BUILD_TYPES).map((type) =>
packageRunner(
'build_window_packages_bundle',
'window',
buildWindowBundle,
constants.BUILD_TYPES[type],
),
);
return series(builds);
}
module.exports = {
build_window_packages: series(
parallel(
packageRunner(
'build_window_packages_version_module',
'window',
versionModule,
),
),
windowBundleSequence(),
),
};
================================================
FILE: gulp-tasks/build.js
================================================
/*
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.
*/
const {series} = require('gulp');
const execa = require('execa');
const fse = require('fs-extra');
const upath = require('upath');
const {build_packages} = require('./build-packages');
// This is needed for workbox-build but is also used by the rollup-helper
// to add CDN details to workbox-sw.
// Make sure this runs **before** build_lerna_bootstrap.
async function build_update_cdn_details() {
const cdnDetails = await fse.readJSON(
upath.join(__dirname, '..', 'cdn-details.json'),
);
const workboxBuildPath = upath.join(
__dirname,
'..',
'packages',
'workbox-build',
);
const workboxBuildCdnDetailsPath = upath.join(
workboxBuildPath,
'src',
'cdn-details.json',
);
const workboxBuildPkg = await fse.readJSON(
upath.join(workboxBuildPath, 'package.json'),
);
cdnDetails.latestVersion = workboxBuildPkg.version;
await fse.writeJson(workboxBuildCdnDetailsPath, cdnDetails, {
spaces: 2,
});
}
async function build_lerna_bootstrap() {
await execa('lerna', ['bootstrap'], {preferLocal: true});
}
module.exports = {
build: series(
build_update_cdn_details,
build_lerna_bootstrap,
build_packages,
),
};
================================================
FILE: gulp-tasks/docs.js
================================================
/*
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.
*/
const {series, watch} = require('gulp');
const execa = require('execa');
const fse = require('fs-extra');
const upath = require('upath');
const logHelper = require('../infra/utils/log-helper');
const DOCS_DIRECTORY = upath.join(__dirname, '..', 'docs');
async function docs_build() {
await fse.remove(DOCS_DIRECTORY);
const queryString = [
`projectRoot=/`,
`basepath=/`,
`productName=Workbox`,
].join('&');
const params = [
'-c',
upath.join(__dirname, '..', 'jsdoc.conf'),
'-d',
DOCS_DIRECTORY,
];
if (!(global.cliOptions && global.cliOptions.pretty)) {
logHelper.warn(`
These docs will look ugly, but they will more accurately match what
is shown on developers.google.com.
You can view a friendlier UI by running
'gulp docs --pretty'
`);
params.push(
'--template',
upath.join(
__dirname,
'..',
'infra',
'templates',
'reference-docs',
'jsdoc',
),
'--query',
queryString,
);
}
if (global.cliOptions && global.cliOptions.debugDocs) {
params.push('--debug');
}
await execa('jsdoc', params, {preferLocal: true});
}
function docs_watch() {
const watcher = watch('packages/**/*', docs_build);
watcher.on('error', (err) => logHelper.error(`Docs failed to build: `, err));
}
module.exports = {
docs_build,
docs: series(docs_build, docs_watch),
};
================================================
FILE: gulp-tasks/lint.js
================================================
/*
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.
*/
const {parallel} = require('gulp');
const execa = require('execa');
async function lint_js() {
await execa(
'eslint',
[
'**/*.{js,mjs}',
'--config',
'javascript.eslintrc.js',
'--ignore-path',
'.gitignore',
],
{preferLocal: true},
);
}
async function lint_ts() {
await execa(
'eslint',
[
'**/*.ts',
'--config',
'typescript.eslintrc.js',
'--ignore-path',
'.gitignore',
],
{preferLocal: true},
);
}
module.exports = {
lint_js,
lint_ts,
// Temporarily disable lint_ts until we upgrade our ESLint dependencies.
lint: parallel(lint_js, lint_ts),
};
================================================
FILE: gulp-tasks/publish-cdn.js
================================================
/*
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.
*/
const upath = require('upath');
const cdnUploadHelper = require('./utils/cdn-helper');
const githubHelper = require('./utils/github-helper');
const logHelper = require('../infra/utils/log-helper');
const publishHelpers = require('./utils/publish-helpers');
// Git adds 'v' to tag name, lerna does not in package.json version.
// We are going to publish to CDN *without* the 'v'
function cleanTagName(name) {
let friendlyTagName = name;
if (friendlyTagName.startsWith('v')) {
friendlyTagName = friendlyTagName.substring(1);
}
return friendlyTagName;
}
async function findMissingCDNTags(tagsData) {
const missingTags = [];
for (const tagData of tagsData) {
const exists = await cdnUploadHelper.tagExists(cleanTagName(tagData.name));
if (!exists) {
missingTags.push(tagData);
}
}
return missingTags;
}
async function handleCDNUpload(tagName, gitBranch) {
const buildDir = await publishHelpers.groupBuildFiles(tagName, gitBranch);
const friendlyTagName = cleanTagName(tagName);
logHelper.log(`Uploading '${tagName}' to the CDN as ${friendlyTagName}.`);
const urls = await cdnUploadHelper.upload(friendlyTagName, buildDir);
logHelper.log('The following URLs are now available:');
for (const url of urls) {
// Skip the .map for cleaner logs.
if (upath.extname(url) !== '.map') {
logHelper.log(`• ${url}`);
}
}
}
async function publish_cdn() {
let missingTags = [];
// Usage: npx gulp publish_cdn --cdnTag=vX.Y.Z
// See https://github.com/GoogleChrome/workbox/issues/2479
if (global.cliOptions.cdnTag) {
const tags = [{name: global.cliOptions.cdnTag}];
missingTags = await findMissingCDNTags(tags);
} else {
// Get all of the tags in the repo.
const tags = await githubHelper.getTags();
missingTags = await findMissingCDNTags(tags);
if (missingTags.length === 0) {
logHelper.log('No tags missing from CDN.');
}
}
for (const missingTag of missingTags) {
// Override the git branch here since we aren't actually
// using a tagged release.
await handleCDNUpload(missingTag.name, missingTag.name);
}
}
module.exports = {
publish_cdn,
};
================================================
FILE: gulp-tasks/publish-github.js
================================================
/*
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.
*/
const semver = require('semver');
const githubHelper = require('./utils/github-helper');
const logHelper = require('../infra/utils/log-helper');
async function publishReleaseOnGithub(tagName, releaseInfo) {
if (!releaseInfo) {
const releaseData = await githubHelper.createRelease({
tag_name: tagName,
draft: true,
name: `Workbox ${tagName}`,
prerelease: semver.prerelease(tagName) !== null,
});
releaseInfo = releaseData.data;
}
}
async function handleGithubRelease(tagName, gitBranch, releaseInfo) {
logHelper.log(`Creating GitHub release ${logHelper.highlight(tagName)}.`);
await publishReleaseOnGithub(tagName, releaseInfo);
}
function filterTagsWithReleaseBundles(allTags, taggedReleases) {
return allTags.filter((tagInfo) => {
const release = taggedReleases[tagInfo.name];
if (release && release.assets.length > 0) {
// If a tag has a release and there is an asset let's assume the
// the release is fine. Note: GitHub's source doesn't count as an
// asset
return false;
}
return true;
});
}
async function publish_github() {
// Get all of the tags in the repo.
const allTags = await githubHelper.getTags();
const taggedReleases = await githubHelper.getTaggedReleases();
const tagsToBuild = filterTagsWithReleaseBundles(allTags, taggedReleases);
if (tagsToBuild.length === 0) {
logHelper.log(`No tags missing from GitHub.`);
return;
}
for (const tagToBuild of tagsToBuild) {
await handleGithubRelease(
tagToBuild.name,
tagToBuild.name,
taggedReleases[tagToBuild.name],
);
}
}
module.exports = {
publish_github,
};
================================================
FILE: gulp-tasks/publish-glitch.js
================================================
/*
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.
*/
const del = require('del');
const execa = require('execa');
const fse = require('fs-extra');
const globby = require('globby');
const ol = require('common-tags').oneLine;
const olt = require('common-tags').oneLineTrim;
const tempy = require('tempy');
const upath = require('upath');
const logHelper = require('../infra/utils/log-helper');
const DEMOS_DIR = 'demos/src';
async function publish_glitch() {
const glitchProjects = await globby('*', {cwd: DEMOS_DIR, onlyFiles: false});
if (!process.env.GLITCH_PERSONAL_TOKEN) {
throw new Error(ol`You must set a GLITCH_TOKEN in your environment to
publish to Glitch (you must have owner or editor access for the
demo associated with the token).`);
}
if (!process.env.GLITCH_WORKBOX_SECRET) {
throw new Error(ol`You must set the correct GLITCH_SECRET in your
environment to publish to Workbox Demos on Glitch.`);
}
for (const project of glitchProjects) {
const projectURL = olt`https://${process.env.GLITCH_PERSONAL_TOKEN}
@api.glitch.com/git/${project}`;
const projectPath = tempy.directory();
try {
await execa('git', ['clone', projectURL, projectPath]);
await fse.copy(upath.join(DEMOS_DIR, project), projectPath, {
overwrite: true,
});
await execa('git', ['checkout', '-b', 'glitch'], {cwd: projectPath});
await execa('git', ['add', '-A'], {cwd: projectPath});
await execa('git', ['commit', `-m'Autocommit on ${new Date()}'`], {
cwd: projectPath,
});
await execa(
'git',
['push', 'origin', 'glitch', '-f', '--set-upstream', '--no-verify'],
{cwd: projectPath},
);
const deployURL = new URL(`https://${project}.glitch.me/deploy`);
deployURL.searchParams.set('secret', process.env.GLITCH_WORKBOX_SECRET);
deployURL.searchParams.set(
'repo',
`https://api.glitch.com/git/${project}`,
);
await execa('curl', ['-X', 'POST', deployURL.href]);
} catch (error) {
logHelper.error(`'${error}' occurred while processing ${project}.`);
}
await del(projectPath, {force: true});
}
}
module.exports = {
publish_glitch,
};
================================================
FILE: gulp-tasks/publish-lerna.js
================================================
/*
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.
*/
const execa = require('execa');
const ol = require('common-tags').oneLine;
const logHelper = require('../infra/utils/log-helper');
async function publish_lerna() {
// See https://github.com/GoogleChrome/workbox/issues/2904#issuecomment-894452253
const options = ['publish', '--force-publish', '--exact'];
// gulp publish --distTag=latest would be the most common.
if (global.cliOptions.distTag) {
logHelper.log(
ol`Using ${logHelper.highlight(
'--dist-tag=' + global.cliOptions.distTag,
)}`,
);
options.push('--dist-tag', global.cliOptions.distTag);
} else {
throw new Error(ol`Please set the --distTag command line option, normally
to 'latest' (for a stable release) or 'next' (for a pre-release).`);
}
await execa('lerna', options, {
preferLocal: true,
stdio: 'inherit',
});
}
module.exports = {
publish_lerna,
};
================================================
FILE: gulp-tasks/publish.js
================================================
/*
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.
*/
const {series} = require('gulp');
const execa = require('execa');
const fse = require('fs-extra');
const ol = require('common-tags').oneLine;
const {build} = require('./build');
const {build_packages_clean} = require('./build-packages');
const {publish_cdn} = require('./publish-cdn');
const {publish_github} = require('./publish-github');
const {publish_lerna} = require('./publish-lerna');
const {test} = require('./test');
const constants = require('./utils/constants');
async function publish_clean() {
await fse.remove(constants.GENERATED_RELEASE_FILES_DIRNAME);
}
async function publish_sign_in_check() {
await execa('npm', ['whoami']);
}
async function dist_tag_check() {
if (!global.cliOptions.distTag) {
throw new Error(ol`Please set the --distTag command line option, normally
to 'latest' (for a stable release) or 'next' (for a pre-release).`);
}
}
module.exports = {
publish: series(
dist_tag_check,
publish_sign_in_check,
build_packages_clean,
publish_clean,
build,
test,
publish_lerna,
publish_github,
publish_cdn,
),
};
================================================
FILE: gulp-tasks/test-integration.js
================================================
/*
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.
*/
const clearModule = require('clear-module');
const execa = require('execa');
const {globSync} = require('glob');
const ol = require('common-tags').oneLine;
const upath = require('upath');
const seleniumAssistant = require('selenium-assistant');
const constants = require('./utils/constants');
const logHelper = require('../infra/utils/log-helper');
const server = require('../infra/testing/server/index');
function runFiles(filePaths) {
// Mocha can't be run multiple times, which we need for NODE_ENV.
// More info: https://github.com/mochajs/mocha/issues/995
clearModule.all();
const Mocha = require('mocha');
return new Promise((resolve, reject) => {
const mocha = new Mocha({
retries: process.env.TRAVIS || process.env.GITHUB_ACTIONS ? 4 : 1,
timeout: 3 * 60 * 1000,
});
for (const filePath of filePaths) {
mocha.addFile(filePath);
}
// Run the tests.
mocha.run(function (failureCount) {
if (failureCount > 0) {
return reject(new Error(`${failureCount} tests failed.`));
}
resolve();
});
});
}
async function runTestSuite(testPath, nodeEnv, seleniumBrowser, webdriver) {
logHelper.log(ol`Running integration test on ${logHelper.highlight(testPath)}
with NODE_ENV '${nodeEnv}' and browser
'${logHelper.highlight(seleniumBrowser.getPrettyName())}'`);
const options = [];
if (global.cliOptions.grep) {
options.push('--grep', global.cliOptions.grep);
}
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = nodeEnv;
try {
global.__workbox = {
seleniumBrowser,
server,
webdriver,
};
const testFiles = globSync(
upath.join(__dirname, '..', testPath, 'test-*.js'),
);
await runFiles(testFiles);
} finally {
process.env.NODE_ENV = originalNodeEnv;
}
}
async function runIntegrationForBrowser(browser) {
const packagesToTest = globSync(`test/${global.packageOrStar}/integration`);
for (const buildKey of Object.keys(constants.BUILD_TYPES)) {
const webdriver = await browser.getSeleniumDriver();
const timeout = 2 * 60 * 1000;
webdriver.manage().setTimeouts({
implicit: timeout,
pageLoad: timeout,
script: timeout,
});
for (const packageToTest of packagesToTest) {
// Since workbox-google-analytics is deprecated, removing the tests from integration tests.
if (packageToTest.includes('workbox-google-analytics')) {
continue;
}
const nodeEnv = constants.BUILD_TYPES[buildKey];
try {
await runTestSuite(packageToTest, nodeEnv, browser, webdriver);
} catch (error) {
await seleniumAssistant.killWebDriver(webdriver);
throw error;
}
}
await seleniumAssistant.killWebDriver(webdriver);
}
}
async function test_integration() {
if (process.platform === 'win32') {
logHelper.warn(`Skipping integration tests on Windows.`);
return;
}
// Install the latest Chrome and Firefox webdrivers without referencing
// package-lock.json, to ensure that they're up to date.
await execa('npm', ['install', '--no-save', 'chromedriver', 'geckodriver'], {
preferLocal: true,
});
logHelper.log(`Downloading browsers...`);
const expiration = 24;
// We are only running tests in stable, see bellow for reasons.
await seleniumAssistant.downloadLocalBrowser('chrome', 'stable', expiration);
await seleniumAssistant.downloadLocalBrowser('firefox', 'stable', expiration);
const packagesToTest = globSync(`test/${global.packageOrStar}/integration`);
if (packagesToTest.length === 0) {
return;
}
await server.start();
try {
const localBrowsers = seleniumAssistant.getLocalBrowsers();
for (const localBrowser of localBrowsers) {
switch (localBrowser.getId()) {
case 'firefox':
if (localBrowser.getReleaseName() !== 'unstable') {
await runIntegrationForBrowser(localBrowser);
}
break;
// Temporarily only test the stable release of Chrome, until the next
// https://www.npmjs.com/package/chromedriver release.
case 'chrome':
case 'safari':
if (localBrowser.getReleaseName() === 'stable') {
await runIntegrationForBrowser(localBrowser);
}
break;
default:
logHelper.warn(ol`Skipping integration tests for
${localBrowser.getPrettyName()}.`);
}
}
} finally {
await server.stop();
}
}
module.exports = {
test_integration,
};
================================================
FILE: gulp-tasks/test-node.js
================================================
/*
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.
*/
const {series} = require('gulp');
const execa = require('execa');
const fse = require('fs-extra');
const {globSync} = require('glob');
const ol = require('common-tags').oneLine;
const upath = require('upath');
const constants = require('./utils/constants');
const logHelper = require('../infra/utils/log-helper');
async function runNodeTestSuite(testPath, nodeEnv) {
logHelper.log(ol`Running node test on ${logHelper.highlight(testPath)}
with NODE_ENV '${nodeEnv}'`);
const options = [];
if (global.cliOptions.grep) {
options.push('--grep', global.cliOptions.grep);
}
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = nodeEnv;
try {
const {stdout} = await execa(
'nyc',
[
'--clean',
'false',
'--silent',
'mocha',
'--timeout',
'60000',
`${testPath}/**/*.{js,mjs}`,
...options,
],
{preferLocal: true},
);
console.log(stdout);
} finally {
process.env.NODE_ENV = originalNodeEnv;
}
}
async function runNodeTestsWithEnv(testGroup, nodeEnv) {
const globConfig = {
ignore: ['**/all/**'],
};
if (testGroup === 'all') {
globConfig.ignore = [];
}
const packagesToTest = globSync(`test/${testGroup}/node`, globConfig);
for (const packageToTest of packagesToTest) {
// Hardcode special logic for webpack v4 and v5 tests, which need to
// be run in separate processes.
if (packageToTest.includes('workbox-webpack-plugin')) {
await runNodeTestSuite(`${packageToTest}/v4`, nodeEnv);
await runNodeTestSuite(`${packageToTest}/v5`, nodeEnv);
} else {
await runNodeTestSuite(packageToTest, nodeEnv);
}
}
}
async function test_node_prod() {
await runNodeTestsWithEnv(global.packageOrStar, constants.BUILD_TYPES.prod);
}
async function test_node_dev() {
await runNodeTestsWithEnv(global.packageOrStar, constants.BUILD_TYPES.dev);
}
async function test_node_all() {
await runNodeTestsWithEnv('all', constants.BUILD_TYPES.prod);
}
async function test_node_clean() {
await fse.remove(upath.join(__dirname, '..', '.nyc_output'));
}
async function test_node_coverage() {
const runOptions = [];
if (global.packageOrStar !== '*') {
runOptions.push('--include');
runOptions.push(upath.join('packages', global.packageOrStar, '**', '*'));
}
const {stdout} = await execa(
'nyc',
['report', '--reporter', 'lcov', '--reporter', 'text', ...runOptions],
{preferLocal: true},
);
console.log(stdout);
}
module.exports = {
test_node_all,
test_node_coverage,
test_node_dev,
test_node_prod,
test_node: series(
test_node_clean,
test_node_dev,
test_node_prod,
test_node_all,
test_node_coverage,
),
};
================================================
FILE: gulp-tasks/test-server.js
================================================
/*
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.
*/
const {series} = require('gulp');
const {
transpile_typescript,
transpile_typescript_watch,
} = require('./transpile-typescript');
const constants = require('./utils/constants');
const testServer = require('../infra/testing/server/index');
function handleExit() {
testServer.stop();
process.exit(0);
}
function startServer() {
process.env.NODE_ENV = process.env.NODE_ENV || constants.BUILD_TYPES.dev;
const eventNames = [
'exit',
'SIGINT',
'SIGUSR1',
'SIGUSR2',
'uncaughtException',
];
for (const eventName of eventNames) {
process.on(eventName, handleExit);
}
return testServer.start();
}
module.exports = {
test_server: series(
transpile_typescript,
startServer,
transpile_typescript_watch,
),
};
================================================
FILE: gulp-tasks/test.js
================================================
/*
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.
*/
const {parallel, series} = require('gulp');
const {lint} = require('./lint');
const {test_integration} = require('./test-integration');
const {test_node} = require('./test-node');
const logHelper = require('../infra/utils/log-helper');
async function logSkip() {
logHelper.log('Skipping test suite due to --skipTests');
}
function runTestsUnlessSkipped() {
if (global.cliOptions.skipTests) {
return logSkip;
} else {
// The node and integration tests both muck with process.env.NODE_ENV,
// and therefore can't be run in parallel.
return parallel(lint, series(test_node, test_integration));
}
}
module.exports = {
test: runTestsUnlessSkipped(),
};
================================================
FILE: gulp-tasks/transpile-typescript.js
================================================
/*
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.
*/
const execa = require('execa');
const fse = require('fs-extra');
const globby = require('globby');
const upath = require('upath');
const {AsyncDebounce} = require('../infra/utils/AsyncDebounce');
/**
* A mapping between each package name and its corresponding `AsyncDebounce`
* instance of the `transpilePackage()` function.
*
* @type {{[key: string]: AsyncDebounce}}
*/
const debouncedTranspilerMap = {};
/**
* Takes a package name and schedules that package's source TypeScript code
* to be converted to JavaScript. If a transpilation is already scheduled, it
* won't be queued twice, so this function is safe to call as frequently as
* needed.
*
* @param {string} packageName
* @param {Object} [options]
*/
async function queueTranspile(packageName, options) {
if (!debouncedTranspilerMap[packageName]) {
debouncedTranspilerMap[packageName] = new AsyncDebounce(async () => {
await transpile_typescript();
});
}
await debouncedTranspilerMap[packageName].call();
debouncedTranspilerMap[packageName] = null;
}
/**
* A mapping between package names and whether or not they have pending
* file changes
*
* @type {{[key: string]: boolean}}
*/
const pendingChangesMap = {};
/**
* Returns true if a TypeScript file has been modified in the package since
* the last time it was transpiled.
*
* @param {string} packageName
*/
function needsTranspile(packageName) {
return pendingChangesMap[packageName] === true;
}
/**
* Transpiles all packages listed in the root tsconfig.json's references section
* into .js and .d.ts files. Creates stub .mjs files that re-export the contents
* of the .js files.
*
* Unlike other scripts, this does not take the --package= command line param
* into account. Each project in packages/ theoretically could depend on any
* other project, so kicking off a single, top-level compilation makes the
* most sense (and is faster when all the packages need to be compiled).
*/
async function transpile_typescript() {
await execa('tsc', ['--build', 'tsconfig.json'], {preferLocal: true});
const jsFiles = await globby(`packages/*/**/*.js`, {
ignore: ['**/build/**', '**/src/**'],
});
for (const jsFile of jsFiles) {
const {dir, name} = upath.parse(jsFile);
const mjsFile = upath.join(dir, `${name}.mjs`);
const mjsSource = `export * from './${name}.js';`;
await fse.outputFile(mjsFile, mjsSource);
}
}
async function transpile_typescript_watch() {
await execa('tsc', ['--build', '--watch', 'tsconfig.json'], {
preferLocal: true,
});
}
module.exports = {
// These are used as functions from other modules, not as gulp tasks.
functions: {
needsTranspile,
queueTranspile,
},
transpile_typescript_watch,
transpile_typescript,
};
================================================
FILE: gulp-tasks/utils/analyse-properties.js
================================================
/*
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.
*/
/*
* This file should be run as a node script to analyse the content of the
* browser bundles.
*
* It's an extremely naive approach to picking property names out and seeing
* how often they are used in the final browser bundles. This can be
* used to detect variable names that are long and used repeatedly meaning
* that a small refactor allowing `uglify-es` to mangle these properties,
* could lead to reasonable improvement in final bundle size.
*
* This is very rough and vague. It needs to be run manually and if it's not
* used can and should be removed from Workbox repo.
*/
const {globSync} = require('glob');
const path = require('path');
const fs = require('fs-extra');
const babylon = require('babylon');
const constants = require('./constants');
const logHelper = require('../../infra/utils/log-helper');
class AnalyseBuildForProperties {
run() {
const filePaths = this.getBuildFiles();
return Promise.all(
filePaths.map((filePath) => {
const rawAnalysis = this.analyzeFile(filePath);
const analysis = this.tidyData(rawAnalysis);
return {
filePath,
analysis,
};
}),
);
}
getBuildFiles() {
// workbox-sw doesn't include .prod. in the build name.
const buildGlob = path.join(
__dirname,
'..',
'..',
'packages',
'*',
constants.PACKAGE_BUILD_DIRNAME,
'{*.prod.js,workbox-sw.js}',
);
return globSync(buildGlob);
}
analyzeFile(filePath) {
const fileContents = fs.readFileSync(filePath).toString();
const parsedCode = babylon.parse(fileContents);
const tokens = parsedCode.tokens;
const nameTokens = tokens.filter((token) => {
return token.type.label === 'name';
});
const matches = {};
nameTokens.forEach((token) => {
const variableName = token.value;
if (!matches[variableName]) {
matches[variableName] = 0;
}
matches[variableName]++;
});
return Object.keys(matches).map((matchKey) => {
return {
propertyName: matchKey,
propertyCount: matches[matchKey],
};
});
}
tidyData(analysisEntries) {
return analysisEntries
.filter((entry) => {
// If there is only one entry or it's a single character it's
// either not important or it's already been minified.
return entry.propertyCount > 1 && entry.propertyName.length > 1;
})
.filter((entry) => {
switch (entry.propertyName) {
case 'await':
case 'async':
return false;
default:
return true;
}
})
.sort((a, b) => {
return b.propertyCount - a.propertyCount;
});
}
printDetails({filePath, analysis}) {
let longestPropertyName = 0;
analysis.forEach((entry) => {
if (entry.propertyName.length > longestPropertyName) {
longestPropertyName = entry.propertyName.length;
}
});
logHelper.log();
logHelper.log(`Results for '${path.relative(process.cwd(), filePath)}'`);
logHelper.log();
analysis.forEach((entry) => {
const numberOfSpaces = longestPropertyName - entry.propertyName.length;
const extraSpace = ' '.repeat(numberOfSpaces);
logHelper.log(
` ${entry.propertyName} ` + `${extraSpace} ${entry.propertyCount}`,
);
});
logHelper.log();
}
}
module.exports = AnalyseBuildForProperties;
================================================
FILE: gulp-tasks/utils/cdn-helper.js
================================================
/*
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.
*/
const {globSync} = require('glob');
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const cdnDetails = require('../../cdn-details.json');
const logHelper = require('../../infra/utils/log-helper');
const PROJECT_ID = 'workbox-bab1f';
class CDNHelper {
constructor() {
this._gcs = null;
}
_getReleaseTagPath(tagName) {
return `${cdnDetails.releasesDir}/${tagName}`;
}
async getGCS() {
if (!this._gcs) {
// Run `gcloud auth application-default login` if needed.
// See https://stackoverflow.com/a/42059661/385997
this._gcs = new Storage({
projectId: PROJECT_ID,
});
}
return this._gcs;
}
async tagExists(tagName) {
const gcs = await this.getGCS();
try {
const bucket = gcs.bucket(cdnDetails.bucketName);
// bucket.file('some/path/').exists() doesn't seem to work
// for nested directories. Instead we are checking if there are
// files in the expected release directory.
const [files] = await bucket.getFiles({
prefix: `${this._getReleaseTagPath(tagName)}/`,
});
return files.length > 0;
} catch (err) {
logHelper.error(err);
throw err;
}
}
async upload(tagName, directoryToUpload) {
const gcs = await this.getGCS();
const filePaths = globSync(`${directoryToUpload}/*`, {
absolute: true,
});
const publicUrls = [];
const bucket = gcs.bucket(cdnDetails.bucketName);
for (const filePath of filePaths) {
const destination = `${this._getReleaseTagPath(tagName)}/${path.basename(
filePath,
)}`;
try {
await bucket.upload(filePath, {
destination,
gzip: true,
public: true,
resumable: false,
metadata: {
cacheControl: 'public, max-age=31536000',
},
});
} catch (err) {
logHelper.error(`Failed to upload file to GCS bucket: '${filePath}'`);
throw err;
}
publicUrls.push(
`${cdnDetails.origin}/${cdnDetails.bucketName}/${destination}`,
);
}
return publicUrls;
}
}
module.exports = new CDNHelper();
================================================
FILE: gulp-tasks/utils/constants.js
================================================
/*
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.
*/
module.exports = {
// This is a directory that should not be commited
// to git and will be removed and rebuilt between
// test runs.
PACKAGE_BUILD_DIRNAME: 'build',
GENERATED_RELEASE_FILES_DIRNAME: 'generated-release-files',
// This is used in the publish-bundle step to avoid doing anything
// with tags that have known issues.
MIN_RELEASE_TAG_TO_PUBLISH: 'v6.1.5',
GITHUB_OWNER: 'GoogleChrome',
GITHUB_REPO: 'workbox',
// This is the environments that we should use for NODE_ENV.
BUILD_TYPES: {
dev: 'dev',
prod: 'production',
},
};
================================================
FILE: gulp-tasks/utils/get-packages.js
================================================
/*
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.
*/
const {globSync} = require('glob');
const path = require('path');
const DEFAULT_ROOT = path.join(__dirname, '..', '..');
const getPackages = ({type, root = DEFAULT_ROOT} = {}) => {
const pathToPkgJsons = globSync('packages/*/package.json', {cwd: root});
return pathToPkgJsons
.map((pathToPkgJson) => {
const pkg = require(`${path.resolve(root)}/${pathToPkgJson}`);
return pkg;
})
.filter((pkg) => {
return pkg.workbox && pkg.workbox.packageType === type;
});
};
module.exports = {
getPackages,
};
================================================
FILE: gulp-tasks/utils/github-helper.js
================================================
/*
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.
*/
const {Octokit} = require('@octokit/rest');
const semver = require('semver');
const constants = require('./constants');
// github.authenticate() is synchronous, and it only stores the credentials for
// the next request, so it should be called once per method that requires auth.
// See https://github.com/mikedeboer/node-github#authentication
const authenticate = () => {
if (!process.env.GITHUB_TOKEN) {
throw new Error(
'You must set a GITHUB_TOKEN in your environment to ' +
'publish a GitHub release.',
);
}
return new Octokit({auth: process.env.GITHUB_TOKEN});
};
module.exports = {
createRelease: (args) => {
const github = authenticate();
args.owner = constants.GITHUB_OWNER;
args.repo = constants.GITHUB_REPO;
return github.rest.repos.createRelease(args);
},
uploadAsset: (args) => {
const github = authenticate();
args.owner = constants.GITHUB_OWNER;
args.repo = constants.GITHUB_REPO;
return github.rest.repos.uploadReleaseAsset(args);
},
getTaggedReleases: async () => {
const github = authenticate();
const releasesData = await github.rest.repos.listReleases({
owner: constants.GITHUB_OWNER,
repo: constants.GITHUB_REPO,
});
const releases = releasesData.data;
const releasesByTags = {};
releases.forEach((release) => {
const tagName = release.tag_name;
if (semver.gte(tagName, constants.MIN_RELEASE_TAG_TO_PUBLISH)) {
releasesByTags[tagName] = release;
}
});
return releasesByTags;
},
getTags: async () => {
const github = authenticate();
const tagsResponse = await github.repos.listTags({
owner: constants.GITHUB_OWNER,
repo: constants.GITHUB_REPO,
});
const tagsData = tagsResponse.data;
return tagsData.filter((tagData) => {
return semver.gte(tagData.name, constants.MIN_RELEASE_TAG_TO_PUBLISH);
});
},
};
================================================
FILE: gulp-tasks/utils/node-projects-babel.config.json
================================================
{
"presets": [["@babel/preset-env", {"targets": {"node": "10.0"}}]]
}
================================================
FILE: gulp-tasks/utils/output-filename-to-package-map.js
================================================
/*
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.
*/
const {getPackages} = require('./get-packages');
const outputFilenameToPkgMap = {};
const windowAndSWPackages = [
...getPackages({type: 'sw'}),
...getPackages({type: 'window'}),
];
windowAndSWPackages.forEach((pkg) => {
// When no `outputFilename` property exists, the package name is used.
const outputFilename = pkg.workbox.outputFilename || pkg.name;
outputFilenameToPkgMap[outputFilename] = pkg;
});
module.exports = {
outputFilenameToPkgMap,
};
================================================
FILE: gulp-tasks/utils/package-runner.js
================================================
/*
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.
*/
const path = require('path');
const {globSync} = require('glob');
const oneLine = require('common-tags').oneLine;
const pkgPathToName = require('./pkg-path-to-name');
/**
* @param {string} typeFilter The type of packages to return: 'node', 'sw',
* or 'all'.
* @return Array Paths to package.json files for the matching packages.
*/
function getPackages(typeFilter) {
return globSync(`packages/${global.packageOrStar}/package.json`, {
absolute: true,
}).filter((pathToPackageJson) => {
const pkgInfo = require(pathToPackageJson);
const packageType = pkgInfo.workbox.packageType;
if (!packageType) {
throw Error(oneLine`Unable to determine package type. Please add
workbox.packageType metadata to ${pathToPackageJson}`);
}
return typeFilter === 'all' || typeFilter === packageType;
});
}
/*
* This methods only purpose is to call a function with
* the package path that needs to be acted upon.
*
* The specific function might vary depending on whether a give project targets
* Node or the browser, so you can specify 'sw', 'node', or 'all'.
*
* If we ran gulp as `gulp build` we would want the
* 'build' task to run against all packages.
*
* If we ran gulp as `gulp build --project workbox-core`
* we would want the 'build' task to run against `workbox-core`
* **only**.
*
* This method simplifies how we would write the 'build'
* task and should be the only way we write functions.
*
* ```javascript
* gulp.task('build',
* gulp.series(
* packageRunner(displayName, 'all', func, arg1, arg2)
* )
* );
* ```
*
* Package runner will return an array of functions that
* will call `func` where the first argument
* is the absolute path to a package,
* like `/user/matt/workbox/packages/workbox-core` and
* the remaining arguments will be whatever is passed to
* packageRunner, in the above sample this would be
* `arg1` and `arg2`.
*
* In the example above, `func` would be written as:
*
* ```javascript
* function functionForEachProjectType(packagePath, arg1, arg2) {
* // Return a Promise
* return Promise.resolve().then(() => ...)
*
* // OR
*
* // Return a gulp stream
* return gulp.src(path.posix.join(packagePath, 'example-dir', '*.js'))
* .pipe(...)
* .pipe(gulp.dest(...));
* }
* ```
*
* You can use this with both `gulp.series` and
* `gulp.parallel`.
*
* ```javascript
* gulp.series(packageRunner(displayName, 'all', func))
* gulp.parallel(packageRunner(displayName, 'all', func))
* ```
*
* If you need to call the runner with multiple functions
* you can do this and gulp will merge the arrays returned
* by the runner.
*
* For example:
*
* ```javascript
* gulp.parallel(
* packageRunner(displayName, 'sw', func, true),
* packageRunner(displayName, 'sw', func, false),
* )
* ```
*
* If we run the above, it would call the appropriate
* `func` for all packages in workbox twice, once with
* argument `true` and once with `false`. `gulp.parallel` will run all of these
* calls to `func` in parallel.
*
* @param {string} displayName A friendly name to log.
* @param Array The type of package we want to run func against:
* 'node', 'sw', or 'all'.
* @param {function} func The function that we want to run for each package.
* @param {*} args Any arguments that should be passed in to the func.
* @return Array All of the function wrappers.
*/
module.exports = (displayName, packageType, func, ...args) => {
const packagePaths = getPackages(packageType);
// We need to return a single no-op rather than an empty array, or else gulp
// will throw 'One or more tasks should be combined using series or parallel'.
if (packagePaths.length === 0) {
const noOp = (done) => done();
noOp.displayName = displayName;
return [noOp];
}
return packagePaths.map((packagePath) => {
const packageRootPath = path.dirname(packagePath);
const wrappedFunc = () => func(packageRootPath, ...args);
wrappedFunc.displayName = oneLine`
${displayName}
(${pkgPathToName(packageRootPath)})
${args.length === 0 ? '' : JSON.stringify(args)}`;
return wrappedFunc;
});
};
================================================
FILE: gulp-tasks/utils/pkg-path-to-name.js
================================================
/*
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.
*/
const path = require('path');
const packagesPath = path.join(__dirname, '..', '..', 'packages');
// A helper method that should be used when you want to log
// the package name ONLY.
module.exports = (pkgPath) => {
return path.relative(packagesPath, pkgPath);
};
================================================
FILE: gulp-tasks/utils/publish-helpers.js
================================================
/*
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.
*/
const execa = require('execa');
const fse = require('fs-extra');
const {globSync} = require('glob');
const ol = require('common-tags').oneLine;
const upath = require('upath');
const {outputFilenameToPkgMap} = require('./output-filename-to-package-map');
const constants = require('./constants');
const logHelper = require('../../infra/utils/log-helper');
const SOURCE_CODE_DIR = 'source-code';
const GROUPED_BUILD_FILES = 'grouped-build-files';
const doesDirectoryExist = async (directoryPath) => {
let stats = null;
try {
stats = await fse.stat(directoryPath);
} catch (err) {
return false;
}
return stats.isDirectory();
};
const getBuildPath = (tagName) => {
const tempReleasePath = upath.join(
__dirname,
'..',
'..',
constants.GENERATED_RELEASE_FILES_DIRNAME,
);
return upath.join(tempReleasePath, tagName);
};
const downloadGitCommit = async (tagName, gitBranch) => {
if (!tagName) {
throw new Error(`You must provide a tagName to 'downloadGitCommit()`);
}
if (!gitBranch) {
throw new Error(`You must provide a gitBranch to 'downloadGitCommit()`);
}
const sourceCodePath = upath.join(getBuildPath(tagName), SOURCE_CODE_DIR);
logHelper.log(`Download Git Commit ${logHelper.highlight(gitBranch)}.`);
const dirExists = await doesDirectoryExist(sourceCodePath);
if (!dirExists) {
logHelper.log(` Clone Git repo for branch: '${gitBranch}'.`);
await execa('git', [
'clone',
'--branch',
gitBranch,
'--depth',
'1',
`http://github.com/${constants.GITHUB_OWNER}/${constants.GITHUB_REPO}.git`,
sourceCodePath,
]);
} else {
logHelper.log(` Git repo for branch '${gitBranch}' is cloned already.`);
}
return sourceCodePath;
};
const buildGitCommit = async (tagName) => {
const sourceCodePath = upath.join(getBuildPath(tagName), SOURCE_CODE_DIR);
logHelper.log(ol`
Building Commit
${logHelper.highlight(upath.relative(process.cwd(), sourceCodePath))}.
`);
await execa('npm', ['install'], {cwd: sourceCodePath});
await execa('gulp', ['build'], {cwd: sourceCodePath});
// This is to try and fix GitHub and CDN steps from having file read / close
// issues by removing any risk of spawn tasks being out of sync
await new Promise((resolve) => setTimeout(resolve, 1000));
};
/*
* This function will create a directory with the same name as the
* .tar.gz file it generates. This way when the file is extracted
* the folder structure will be the same.
*/
const groupBuildFiles = async (tagName, gitBranch) => {
const groupedBuildFiles = upath.join(
getBuildPath(tagName),
GROUPED_BUILD_FILES,
);
const dirExists = await doesDirectoryExist(groupedBuildFiles);
if (!dirExists) {
await downloadGitCommit(tagName, gitBranch);
await buildGitCommit(tagName);
const sourceCodePath = upath.join(getBuildPath(tagName), SOURCE_CODE_DIR);
const browserPackages = Object.values(outputFilenameToPkgMap).map(
(item) => item.name,
);
const pattern = upath.join(
sourceCodePath,
'packages',
`{${browserPackages.join(',')}}`,
constants.PACKAGE_BUILD_DIRNAME,
'*.{js,mjs,map}',
);
logHelper.log(ol`
Grouping Build Files into
${logHelper.highlight(upath.relative(process.cwd(), groupedBuildFiles))}.
`);
// Copy files from the source code and move into the grouped build
// directory. In others, have a flat file structure of just the built files.
const filesToInclude = globSync(pattern);
for (const fileToInclude of filesToInclude) {
await fse.copy(
fileToInclude,
upath.join(groupedBuildFiles, upath.basename(fileToInclude)),
);
}
} else {
logHelper.log(` Build files already grouped.`);
}
return groupedBuildFiles;
};
module.exports = {
groupBuildFiles,
};
================================================
FILE: gulp-tasks/utils/rollup-helper.js
================================================
/*
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.
*/
const {babel} = require('@rollup/plugin-babel');
const {nodeResolve} = require('@rollup/plugin-node-resolve');
const asyncToPromises = require('babel-plugin-transform-async-to-promises');
const replace = require('@rollup/plugin-replace');
const terserPlugin = require('@rollup/plugin-terser');
const constants = require('./constants');
const getVersionsCDNUrl = require('./versioned-cdn-url');
// See https://github.com/GoogleChrome/workbox/issues/1674
const nameCache = {};
module.exports = {
// Every use of rollup should have minification and the replace
// plugin set up and used to ensure as consist set of tests
// as possible.
getDefaultPlugins: (buildType, buildFormat = 'iife', es5 = false) => {
const plugins = [nodeResolve()];
const babelConfig = {
babelHelpers: 'bundled',
presets: [
[
'@babel/preset-env',
{
loose: true,
targets: {
browsers: es5
? // If es5 is true, target IE11
// https://github.com/browserslist/browserslist#queries
['ie 11']
: // This corresponds to the version of Chromium used by
// Samsung Internet 6.x, which is the minimum non-evergreen
// browser we currently support.
['chrome >= 56'],
},
},
],
],
};
if (es5) {
babelConfig.plugins = [asyncToPromises];
}
plugins.push(babel(babelConfig));
const minifyBuild = buildType === constants.BUILD_TYPES.prod;
if (minifyBuild) {
const terserOptions = {
nameCache,
module: buildFormat === 'esm' ? true : false,
mangle: {
properties: {
reserved: [
// Chai will break unless we reserve this private variable.
'_obj',
// We need this to be exported correctly.
'_private',
// See https://github.com/GoogleChrome/workbox/issues/2686
'_handle',
],
// mangle > properties > regex will allow terser to minify
// private variable and names that start with a single underscore
// followed by a letter. This restriction to avoid mangling
// unintentional fields in our or other libraries code.
regex: /^_[A-Za-z]/,
// If you are getting an error due to a property mangle
// set this flag to true and the property will be changed
// from '_foo' to '$_foo$' to help diagnose the problem.
debug: false,
},
},
};
plugins.push(terserPlugin(terserOptions));
}
// This is what the build should be
const replaceOptions = {
preventAssignment: true,
WORKBOX_CDN_ROOT_URL: getVersionsCDNUrl(),
};
if (buildType) {
replaceOptions['process.env.NODE_ENV'] = JSON.stringify(buildType);
}
// Replace allows us to input NODE_ENV and strip code accordingly
plugins.push(replace(replaceOptions));
return plugins;
},
};
================================================
FILE: gulp-tasks/utils/version-module.js
================================================
/*
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.
*/
const fs = require('fs-extra');
const path = require('path');
const pkgPathToName = require('./pkg-path-to-name');
const getDetails = (packagePath) => {
// Read file from filesystem to avoid require caching
const packageJsonPath = path.join(packagePath, 'package.json');
const pkgJson = fs.readJSONSync(packageJsonPath);
const name = pkgPathToName(packagePath).replace('workbox-', '');
return ['workbox', name, pkgJson.version].join(':');
};
module.exports = async (packagePath) => {
const versionString = `try{self['${getDetails(
packagePath,
)}']&&_()}catch(e){}`;
if (await fs.pathExists(path.join(packagePath, 'src', 'index.ts'))) {
const tsVersionString = `// @ts-ignore\n${versionString}`;
await fs.writeFile(
path.join(packagePath, 'src', '_version.ts'),
tsVersionString,
);
}
const mjsVersionString = `${versionString}// eslint-disable-line`;
return fs.writeFile(path.join(packagePath, '_version.mjs'), mjsVersionString);
};
================================================
FILE: gulp-tasks/utils/versioned-cdn-url.js
================================================
/*
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.
*/
const cdn = require('../../packages/workbox-build/src/cdn-details.json');
const lernaPkg = require('../../lerna.json');
module.exports = () =>
`${cdn.origin}/${cdn.bucketName}/${cdn.releasesDir}` + `/${lernaPkg.version}`;
================================================
FILE: gulpfile.js
================================================
/*
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.
*/
const fse = require('fs-extra');
const globby = require('globby');
const minimist = require('minimist');
const upath = require('upath');
const options = minimist(process.argv.slice(2));
if (options.package) {
// Ensure the package is valid before running tasks
try {
fse.statSync(upath.join(__dirname, 'packages', options.package));
} catch (err) {
throw new Error(`The supplied package '${options.package}' is invalid.`);
}
}
global.port = options.port || 3000;
global.packageOrStar = options.package || '*';
global.cliOptions = options;
const taskFiles = globby.sync('./gulp-tasks/*.js');
for (const taskFile of taskFiles) {
const taskDefinitions = require(taskFile);
for (const [name, task] of Object.entries(taskDefinitions)) {
if (name === 'functions') {
continue;
}
if (name in module.exports) {
throw new Error(
`Duplicate task definition: ${name} defined in` +
` ${taskFile} conflicts with another task.`,
);
}
module.exports[name] = task;
}
}
================================================
FILE: infra/pr-bot/aggregate-size-plugin.js
================================================
/*
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.
*/
const {oneLine} = require('common-tags');
const {PluginInterface} = require('pr-bot');
const bytes = require('bytes');
const fs = require('fs-extra');
const gzipSize = require('gzip-size');
const path = require('path');
// 15kb max size, gzip'ed.
const MAX_SIZE_GZIP = 15 * 1024;
class AggregateSizePlugin extends PluginInterface {
constructor() {
super(`Workbox Aggregate Size Plugin`);
}
async run({afterPath} = {}) {
const packagesToAggregate = [
`workbox-cacheable-response`,
`workbox-core`,
`workbox-expiration`,
`workbox-precaching`,
`workbox-routing`,
`workbox-strategies`,
`workbox-sw`,
];
const absoluteAfterPath = path.resolve(afterPath);
const files = packagesToAggregate.map((pkgName) => {
const prefix = `${absoluteAfterPath}/packages/${pkgName}/`;
const pkgJson = require(`${prefix}package.json`);
const posixPath = prefix + pkgJson.main;
return posixPath.split('/').join(path.sep);
});
let totalSize = 0;
let totalGzipSize = 0;
for (const filePath of files) {
const fileContents = await fs.readFile(filePath);
const stat = await fs.stat(filePath);
totalSize += stat.size;
totalGzipSize += await gzipSize(fileContents);
}
const percentValue = (totalGzipSize / MAX_SIZE_GZIP) * 100;
const percentString = parseFloat(percentValue).toFixed(0);
const totalSizeString = bytes(totalSize);
const totalGzipString = bytes(totalGzipSize);
let markdownWarning = ``;
if (percentValue >= 90) {
const markdownMoji = percentValue >= 95 ? '☠️' : '⚠️';
markdownWarning = oneLine`
${markdownMoji} WARNING ${markdownMoji}
We are using ${percentString}%
of our max budget for gzip'ed bundle size.
`;
}
const failPR = Boolean(percentValue > 100);
const prettyLog =
`**${totalGzipString}** gzip'ed ` +
`(**${percentString}%** of limit)\n` +
`**${totalSizeString}** uncompressed`;
const markdownLog = `${markdownWarning}\n\n${prettyLog}`;
return {
failPR, // Fail the PR if we have exceeded the limit.
markdownLog,
prettyLog,
};
}
}
module.exports = AggregateSizePlugin;
================================================
FILE: infra/templates/reference-docs/jsdoc/lang/en.yaml
================================================
# @license
# 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.
constructor:
prefix: 'new '
returnTypesSeparator: ' returns '
tables:
body:
defaultValue: 'Defaults to {valueString}.'
defaultValueString: '{value}'
eachValueHasProperties: 'Values in {name} have the following properties:'
isOptional: 'Optional'
isRequired: ' '
nonNullable:
long: 'Value must not be null.'
short: 'non-null'
nullable:
long: 'Value may be null.'
short: 'nullable'
repeatable:
long: 'Value may be repeated.'
short: 'repeatable'
unknownType: '?'
emptyCellPlaceholder: ' '
header:
description: 'Description'
name: 'Name'
optional: 'Optional'
type: 'Type'
notApplicable: '—'
params:
all: '({params})'
joiner: |
{items, plural,
=0 {{param}}
other {, {param}}}
optional: '{param}'
repeatable: '...{param}'
headings:
enums: |
{items, plural,
=1 {Enumeration}
other {Enumerations}}
values: |
{items, plural,
=1 {Value}
other {Values}}
================================================
FILE: infra/templates/reference-docs/jsdoc/lib/publishjob.js
================================================
/*
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.
*/
/* eslint-env node */
/* eslint-disable valid-jsdoc, require-jsdoc */
const name = require('jsdoc/name');
const BaselinePublishJob = require('jsdoc-baseline/lib/publishjob');
class PublishJob extends BaselinePublishJob {
constructor(...args) {
super(...args);
this._symbolNames = {};
this._data;
}
generateTocYaml(symbols, basepath) {
symbols.sort(function (a, b) {
let aName = a.kind === 'namespace' ? a.longname : a.name;
let bName = b.kind === 'namespace' ? b.longname : b.name;
aName = aName.toLowerCase();
bName = bName.toLowerCase();
if (aName > bName) {
return 1;
}
if (aName < bName) {
return -1;
}
return 0;
});
// check for duplicated names
symbols.forEach((symbol) => {
const symbolName =
symbol.kind === 'namespace' ? symbol.longname : symbol.name;
if ({}.hasOwnProperty.call(this._symbolNames, symbolName)) {
this._symbolNames[symbolName] = true;
} else {
this._symbolNames[symbolName] = false;
}
});
// To disambiguate duplicated names, prepend the name and scope of the
// parent. For example, if you have foo.bar.Qux and foo.baz.Qux, the
// disambiguated names are bar.Quxand baz.Qux.
symbols.forEach((symbol) => {
let mappedName = null;
const symbolName =
symbol.kind === 'namespace' ? symbol.longname : symbol.name;
if (this._symbolNames[symbolName] === true) {
if (symbol.kind !== 'namespace') {
// get info for the parent's name
const atomized = name.shorten(symbol.longname);
const atomizedParent = name.shorten(atomized.memberof);
// prepend the parent's name and the symbol's scope to the name
mappedName = atomizedParent.name + atomized.scope + symbol.name;
} else {
mappedName = symbol.longname;
}
}
symbol.tocYamlName =
mappedName ||
(symbol.kind === 'namespace' ? symbol.longname : symbol.name);
});
const data = {
symbols,
basepath,
};
this.generate('toc-yaml', data, '_toc.yaml');
}
generateIndex(readme) {
const data = {
allLongnamesTree: this.allLongnamesTree,
package: this.package,
pageTitle: global.env.opts.query ? global.env.opts.query.productName : '',
readme: readme || null,
};
this.generate('index', data, this.indexUrl);
return this;
}
generateIndexAll(longnames) {
longnames.sort();
const data = {
longnames: longnames,
pageTitle: global.env.opts.query ? global.env.opts.query.productName : '',
};
this.generate('index-all', data, 'index-all.html');
}
}
/** const PublishJob = module.exports = function(...args) {
Parent.apply(this, [].slice.call(args, 0));
};
util.inherits(PublishJob, Parent);**/
module.exports = PublishJob;
================================================
FILE: infra/templates/reference-docs/jsdoc/publish.js
================================================
/*
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.
*/
/* eslint-env node */
const baselineConfig = require('jsdoc-baseline/lib/config');
const fileFinder = require('jsdoc-baseline/lib/filefinder');
const helper = require('jsdoc/util/templateHelper');
const path = require('jsdoc/path');
exports.publish = function (data, opts, tutorials) {
baselineConfig.loadSync();
baselineConfig
.set('l10n', path.join(__dirname, 'lang'))
.set('static', path.join(__dirname, 'static'))
.set('views.partials', path.join(__dirname, 'views'))
.set('views.layouts', path.join(__dirname, 'views'))
.set('modules', path.join(__dirname, 'lib'));
const config = baselineConfig.get();
// load the core modules using the file finder
const modulesFinder = fileFinder.get('modules', config.modules);
const DocletHelper = modulesFinder.require('./doclethelper');
const PublishJob = modulesFinder.require('./publishjob');
const Template = modulesFinder.require('./template');
const docletHelper = new DocletHelper();
const template = new Template(config);
const job = new PublishJob(template, opts);
// set up tutorials
helper.setTutorials(tutorials);
docletHelper.addDoclets(data);
job
.setPackage(docletHelper.getPackage())
.setNavTree(docletHelper.navTree)
.setAllLongnamesTree(docletHelper.allLongnamesTree);
// create the output directory so we can start generating files
job
.createOutputDirectory()
// then generate the source files so we can link to them
.generateSourceFiles(docletHelper.shortPaths);
// generate globals page if necessary
job.generateGlobals(docletHelper.globals);
// generate TOC data and index page
job
.generateTocData({hasGlobals: docletHelper.hasGlobals()})
.generateIndex(opts.readme);
// generate the rest of the output files (excluding tutorials)
docletHelper.getOutputLongnames().forEach(function (longname) {
job.generateByLongname(
longname,
docletHelper.getLongname(longname),
docletHelper.getMemberof(longname),
);
});
// finally, generate the tutorials, and copy static files to the output
// directory
job.generateTutorials(tutorials).copyStaticFiles();
// custom Cast pages
job.generateTocYaml(
docletHelper
.getCategory('classes')
.concat(docletHelper.getCategory('namespaces'))
.concat(docletHelper.getCategory('interfaces')),
global.env.opts.query ? global.env.opts.query.basepath : '',
);
job.generateIndexAll(Object.keys(docletHelper.longname));
};
================================================
FILE: infra/templates/reference-docs/jsdoc/static/jsdoc.css
================================================
.label {
font-style: italic;
text-transform: uppercase;
}
.dl-compact dt {
margin: 8px 0px 0px 0px;
}
h1.devsite-page-title {
display: none;
}
h1 {
margin-bottom: 8px;
}
h2 {
margin: 48px 0px 0px 0px;
}
h3.symbol-name {
margin: 32px 0px 8px 0px;
}
h3.class-list:first-child {
display: block;
margin: 16px 0px 0px 0px;
}
h3.class-list {
display: block;
margin: 12px 0px 0px 0px;
font-size: 16px;
}
h3.class-list + p {
margin: 0px 0px 0px 0px;
}
h4 {
margin: 8px 0px 0px 0px;
}
td p {
margin: 6px 0px 0px 0px;
}
p.type-signature {
font-family: 'Roboto Mono', monospace;
font-size: 80%;
font-style: normal;
font-variant: normal;
font-weight: 500;
font-stretch: normal;
line-height: normal;
margin-top: 0px;
}
p.symbol-index-name {
margin: 8px 0px 0px 0px;
}
h2.symbol-header {
margin: 36px 0px 0px 0px;
}
table.function.param tr {
background: #80a7b9;
}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/augments.hbs
================================================
{{#any augments}}
{{translateHeading 'augments' augments}}
{{#each augments}}
{{link this}}
{{/each}}
{{/any}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/classes-links.hbs
================================================
{{#any items}}
{{translateHeading headingKey items}}
{{#each items}}
{{link longname name}}
{{#if classdesc}}
{{#markdown}}{{resolveLinks classdesc}}{{/markdown}}
{{else}}
{{#markdown}}{{resolveLinks description}}{{/markdown}}
{{/if}}
{{/each}}
{{/any}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/details-table-row.hbs
================================================
{{#block 'details-table-name'}}
{{#if name}}{{name}}{{else}}{{translate 'tables.notApplicable'}}{{/if}}
{{/block}}
{{#block 'details-table-optional'}}
{{#if optional}}
{{translate 'tables.body.isOptional'}}
{{/if}}
{{/block}}
{{#block 'details-table-types'}}
{{#if type}}
{{#all type type.parsedType}}
{{describeType type.parsedType 'extended'}}
{{/all}}
{{/if}}
{{/block}}
{{#block 'details-table-description'}}
{{#if description}}
{{#markdown}}{{resolveLinks description}}{{/markdown}}
{{/if}}
{{#if (hasModifiers this)}}
{{modifierText this}}
{{/if}}
{{#if ../children}}
{{translate 'tables.body.eachValueHasProperties' name=name}}
{{#withOnly values=../children}}
{{#embed 'details-table'}}{{/embed}}
{{/withOnly}}
{{/if}}
{{/block}}
{{!-- This produces extra 's in the output
{{#markdown}}{{resolveLinks description}}{{/markdown}}
--}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/details-table.hbs
================================================
{{#block 'details-table-header'}}
{{#if isEnum}}
{{translateHeading 'values' params}}
{{else}}
{{translateHeading 'parameters' params}}
{{/if}}
{{/block}}
{{#block 'details-table-body'}}
{{#each values}}
{{#embed 'details-table-row'}}{{/embed}}
{{/each}}
{{/block}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/implements.hbs
================================================
{{#any implements}}
{{translateHeading 'implements' implements}}
{{#each implements}}
{{link this}}
{{/each}}
{{/any}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/index-all.hbs
================================================
{{#extend 'layout'}}
{{#content 'title'}}
Index All
{{/content}}
{{#content 'body-main-content'}}
{{#each longnames}}
{{link this}}
{{/each}}
{{/content}}
{{/extend}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/index.hbs
================================================
{{#extend 'layout'}}
{{#content 'title'}}
Overview
{{/content}}
{{#content 'body-main-content'}}
{{#if readme}}
{{{ readme }}}
{{else}}
{{#block 'body-symbol-index'}}
{{#embed 'symbol-index'}}{{/embed}}
{{/block}}
{{/if}}
{{/content}}
{{/extend}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/layout.hbs
================================================
{{#block 'head'}}
{{#block 'meta'}}
{{/block}}
{{#block 'title'}}
{{translatePageTitle pageTitlePrefix pageTitle pageCategory}}
{{/block}}
{{#block 'css'}}
{{/block}}
{{/block}}
{{#block 'body'}}
{{#block 'body-container'}}
{{#block 'body-content'}}
{{#block 'body-content-container'}}
{{#block 'body-banner'}}
{{#block 'body-banner-content'}}
{{!--
Override the `body-banner-content` block
to add content.
--}}
{{/block}}
{{/block}}
{{#block 'body-main'}}
{{!-- Adding a section here causes heading to bump down H1 > H2, etc. --}}
{{#block 'body-main-content'}}
{{!--
Override the `body-main-content` block
to add content.
--}}
{{/block}}
{{/block}}
{{/block}}
{{#block 'body-toc-navbar'}}
{{!-- content is inserted on page load --}}
{{/block}}
{{/block}}
{{/block}}
{{#block 'body-footer'}}
{{/block}}
{{!-- No scripts in Devsite --}}
{{/block}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/params.hbs
================================================
{{#any params}}
{{#withOnly values=(reparentItems this 'params')}}
{{#embed 'details-table'}}{{/embed}}
{{/withOnly}}
{{/any}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/properties.hbs
================================================
{{#any (filterProperties properties)}}
{{#unless isEnum}}
{{translateHeading 'properties' properties}}
{{/unless}}
{{#withOnly values=(reparentItems this 'properties') isEnum=isEnum}}
{{#embed 'details-table'}}{{/embed}}
{{/withOnly}}
{{/any}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/see.hbs
================================================
{{#any see}}
{{translateHeading 'see' see}}
{{#each see}}
{{link (see this ../longname)}}
{{/each}}
{{/any}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/signature.hbs
================================================
{{!--
Note that this section omits the return type for:
+ Constructors
+ Methods with no explicit return type
--}}
{{#with symbol}}
{{~#if (needsSignature this)~}}
{{!-- CONSTRUCTOR PREFIX --}}
{{#is kind 'class'}}
{{#embed 'constructor-prefix'}}{{/embed}}
{{/is}}
{{name}}{{~formatParams params~}}
{{~#isnt kind 'class'~}}
{{~#if (returnTypes this)~}}
{{~translate 'returnTypesSeparator'~}}
{{describeType (returnTypes this)~}}
{{~/if~}}
{{~/isnt~}}
{{~else~}}
{{~#if type~}}
{{~describeType type.parsedType~}}
{{~/if~}}
{{~/if~}}
{{/with}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-content.hbs
================================================
{{#block 'symbol-content-classes'}}
{{#withOnly items=members.classes headingKey='classes'}}
{{#embed 'classes-links'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-interfaces'}}
{{#withOnly items=members.interfaces headingKey='interfaces'}}
{{#embed 'members-links'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-namespaces'}}
{{#withOnly items=members.namespaces headingKey='namespaces'}}
{{#embed 'members-links'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-enums'}}
{{#withOnly items=(where members.properties isEnum=true) headingKey='enums'}}
{{#embed 'members-details'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-properties'}}
{{#withOnly items=(where members.properties isEnum=undefined) headingKey='properties'}}
{{#embed 'members-details'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-methods'}}
{{#withOnly items=members.functions headingKey='functions'}}
{{#embed 'members-details'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-typedefs'}}
{{#withOnly items=members.typedefs headingKey='typedefs'}}
{{#embed 'members-details'}}{{/embed}}
{{/withOnly}}
{{/block}}
{{#block 'symbol-content-events'}}
{{#withOnly items=members.events headingKey='events'}}
{{#embed 'members-details'}}{{/embed}}
{{/withOnly}}
{{/block}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-detail.hbs
================================================
{{~#unless symbol.hideconstructor~}}
{{~#with symbol~}}
{{name}}
{{~/with~}}
{{~/unless~}}
{{!--
Note that we omit the labels for classes, modules, and namespaces, since
these labels would duplicate the labels for the page's main heading.
--}}
{{~#if symbol.kind~}}
{{~#contains 'class' 'module' 'namespace' value=symbol.kind~}}
{{~else~}}
{{~#embed 'symbol-labels'}}{{/embed~}}
{{~/contains~}}
{{~/if~}}
{{~#unless symbol.hideconstructor~}}
{{~#embed 'signature'}}{{/embed~}}
{{~/unless~}}
{{#with symbol}}
{{~#unless symbol.hideconstructor~}}
{{#embed 'description'}}{{/embed}}
{{#embed 'params'}}{{/embed}}
{{#embed 'properties'}}{{/embed}}
{{#embed 'details'}}{{/embed}}
{{#embed 'examples'}}{{/embed}}
{{~/unless~}}
{{/with}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-header.hbs
================================================
{{#block 'symbol-header-heading'}}
{{~#isnt longname memberof}}{{ancestors longname}} {{/isnt~}}
{{name}}
{{~/block}}
{{~#withOnly symbol=this~}}
{{~#embed 'symbol-labels'}}{{/embed~}}
{{/withOnly~}}
{{#block 'symbol-header-classdesc'}}
{{#if classdesc}}
{{#markdown}}{{resolveLinks classdesc}}{{/markdown}}
{{/if}}
{{/block}}
{{#block 'symbol-header-description'}}
{{!--
We don't put a description here for classes, or for namespaces that are
also functions.
--}}
{{#isnt kind 'class'}}
{{#is kind 'namespace'}}
{{#unless (needsSignature this)}}
{{#embed 'description'}}{{/embed}}
{{/unless}}
{{else}}
{{#embed 'description'}}{{/embed}}
{{/is}}
{{/isnt}}
{{/block}}
{{#block 'symbol-header-class-details'}}
{{!--
For classes where we're not displaying the constructor, we show the
details here.
--}}
{{#if hideconstructor}}
{{#embed 'details'}}{{/embed}}
{{/if}}
{{/block}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-index-section.hbs
================================================
{{#each doclets}}
{{linkLongnameWithSignature this '!symbol-index-name'}}
{{/each}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-index.hbs
================================================
{{#eachIndexGroup allLongnamesTree}}
{{@groupName}}
{{#each (group this 3)}}
{{#withOnly doclets=this}}
{{#embed 'symbol-index-section'}}{{/embed}}
{{/withOnly}}
{{/each}}
{{/eachIndexGroup}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-labels.hbs
================================================
{{~#withOnly allLabels=(labels symbol)~}}
{{~#any allLabels~}}
{{~#each allLabels~}}
{{text}}
{{~#unless @last}} {{/unless~}}
{{~/each~}}
{{~/any~}}
{{~/withOnly~}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/symbol-overview.hbs
================================================
{{#any docs}}
{{#first docs}}
{{#embed 'symbol-header'}}{{/embed}}
{{/first}}
{{/any}}
{{#each docs}}
{{#if @first}}
{{!-- heading that indicates what we're about to list --}}
{{#is pageCategory 'classes'}}
{{else}}
{{#any exports}}
{{translateHeading 'exports' docs}}
{{/any}}
{{/is}}
{{/if}}
{{!--
If a module exports only one symbol, document the symbol in the overview, since it's not a
member of anything else.
--}}
{{#is kind 'module'}}
{{#if exports}}
{{#each exports}}
{{#withOnly symbol=this}}
{{#embed 'symbol-detail'}}{{/embed}}
{{/withOnly}}
{{/each}}
{{/if}}
{{!-- Classes also need additional information in the overview. --}}
{{else is kind 'class'}}
{{#unless hideconstructor}}
Constructor
{{#withOnly symbol=this}}
{{#embed 'symbol-detail'}}{{/embed}}
{{/withOnly}}
{{/unless}}
{{!-- Externals also need additional information in the overview. --}}
{{else is kind 'external'}}
{{#with this}}
{{#embed 'details'}}{{/embed}}
{{/with}}
{{!-- Namespaces that are functions also need additional information in the overview. --}}
{{else is kind 'namespace'}}
{{#if (needsSignature this)}}
{{#withOnly symbol=this}}
{{#embed 'symbol-detail'}}{{/embed}}
{{/withOnly}}
{{/if}}
{{/is}}
{{#if @last}}
{{#is pageCategory 'classes'}}
{{else}}
{{#any exports}}
{{/any}}
{{/is}}
{{/if}}
{{/each}}
================================================
FILE: infra/templates/reference-docs/jsdoc/views/toc-yaml.hbs
================================================
toc:
{{#each symbols~}}
- title: "{{tocYamlName}}"
path: {{../basepath}}/{{basename (url longname) '.html'}}
{{/each}}
- title: "Index of all"
path: {{basepath}}/index-all
================================================
FILE: infra/testing/activate-and-control.js
================================================
/*
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.
*/
const activateSWSafari = require('./activate-sw-safari');
// TODO(philipwalton): remove this in favor of using workbox-window.
module.exports = async (swURL) => {
if (global.__workbox.seleniumBrowser.getId() === 'safari') {
return activateSWSafari(swURL);
}
const error = await global.__workbox.webdriver.executeAsyncScript(
(swURL, cb) => {
function _onStateChangePromise(registration, desiredState) {
return new Promise((resolve, reject) => {
if (desiredState === 'activated' && registration.active) {
resolve();
return;
}
if (registration.installing === null) {
reject(new Error('No installing service worker found.'));
return;
}
const serviceWorker = registration.installing;
// We unregister all service workers after each test - this should
// always trigger an install state change
const stateChangeListener = (event) => {
if (event.target.state === desiredState) {
serviceWorker.removeEventListener(
'statechange',
stateChangeListener,
);
resolve();
return;
}
if (event.target.state === 'redundant') {
serviceWorker.removeEventListener(
'statechange',
stateChangeListener,
);
reject(new Error('Installing service worker became redundant.'));
return;
}
};
serviceWorker.addEventListener('statechange', stateChangeListener);
});
}
navigator.serviceWorker
.register(swURL)
.then((registration) => {
return _onStateChangePromise(registration, 'activated');
})
.then(() => {
// Ensure the page is being controlled by the SW.
if (
navigator.serviceWorker.controller &&
navigator.serviceWorker.controller.scriptURL === swURL
) {
return;
} else {
return new Promise((resolve) => {
navigator.serviceWorker.addEventListener(
'controllerchange',
() => {
if (navigator.serviceWorker.controller.scriptURL === swURL) {
resolve();
}
},
);
});
}
})
.then(() => cb())
.catch((error) => cb(error.message));
},
swURL,
);
if (error) {
throw new Error(error);
}
};
================================================
FILE: infra/testing/activate-sw-safari.js
================================================
/*
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.
*/
// TODO(philipwalton): remove this in favor of using workbox-window.
module.exports = async (swURL) => {
// First step: Wait for the page to activate
let error = await global.__workbox.webdriver.executeAsyncScript(
(swURL, cb) => {
const onStateChangePromise = (registration, desiredState) => {
return new Promise((resolve, reject) => {
if (
desiredState === 'activated' &&
registration.active &&
// Checking that the URLs match is needed to fix:
// https://github.com/GoogleChrome/workbox/issues/1633
registration.active.scriptURL === swURL
) {
resolve();
return;
}
const serviceWorker = registration.installing || registration.waiting;
if (serviceWorker === null) {
throw new Error(
'There is no installing or waiting service worker.',
);
}
const stateChangeListener = function (evt) {
if (evt.target.state === desiredState) {
serviceWorker.removeEventListener(
'statechange',
stateChangeListener,
);
resolve();
return;
}
if (evt.target.state === 'redundant') {
serviceWorker.removeEventListener(
'statechange',
stateChangeListener,
);
// Must call reject rather than throw error here due to this
// being inside the scope of the callback function stateChangeListener
reject(new Error('The new service worker became redundant.'));
return;
}
};
serviceWorker.addEventListener('statechange', stateChangeListener);
});
};
navigator.serviceWorker
.register(swURL)
.then((registration) => onStateChangePromise(registration, 'activated'))
.then(() => cb())
.catch((err) => cb(err.message));
},
swURL,
);
if (error) {
throw error;
}
// To be 100% certain - ensure the SW is controlling the page.
error = await global.__workbox.webdriver.executeAsyncScript((swURL, cb) => {
if (
navigator.serviceWorker.controller &&
navigator.serviceWorker.controller.scriptURL === swURL
) {
cb();
} else if (!navigator.serviceWorker.controller) {
cb(`There's no service worker controlling the page.`);
} else {
cb(
`There's an unexpected SW controlling the page: ${navigator.serviceWorker.controller.scriptURL}`,
);
}
}, swURL);
if (error) {
throw error;
}
};
================================================
FILE: infra/testing/auto-stub-logger.mjs
================================================
/*
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 sinon from 'sinon';
import {logger} from '../../packages/workbox-core/_private/logger.mjs';
const sandbox = sinon.createSandbox();
const stubLogger = () => {
// Logger will be `null` in production mode.
if (logger) {
sandbox.stub(logger);
}
};
// Silence any early messages (Normally caused by logging from an import at
// the top of a test)
stubLogger();
// This is part of the "root" mocha suite - meaning it'll reset all the logger
// values before every test.
beforeEach(function () {
sandbox.restore();
stubLogger();
});
after(function () {
sandbox.restore();
});
================================================
FILE: infra/testing/clean-sw.js
================================================
/*
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.
*/
const runInSW = require('./comlink/node-interface');
module.exports = async (webdriver, testingURL) => {
await webdriver.get(testingURL);
try {
await runInSW('clearAllCaches');
} catch (ignored) {
// There might not yet be a service worker registered, in which case we
// can't call runInSW().
}
const error = await webdriver.executeAsyncScript((cb) => {
navigator.serviceWorker
.getRegistration()
.then((reg) => {
if (reg) {
return reg.unregister();
}
})
.then(() => cb())
.catch((err) => cb(err.message));
});
if (error) {
throw new Error(error);
}
await webdriver.get(testingURL);
};
================================================
FILE: infra/testing/comlink/node-interface.js
================================================
/*
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.
*/
module.exports = async (command, ...args) => {
const result = await global.__workbox.webdriver.executeAsyncScript(
(command, args, cb) => {
if (!('_runInSW' in window)) {
cb({error: '_runInSW is not initialized.'});
} else {
window._runInSW[command](...args)
.then((result) => cb(result))
.catch((error) =>
cb({
error: `While running ${command}(${args}): ${error.message}`,
}),
);
}
},
command,
args,
);
if (result instanceof Object && 'error' in result) {
throw new Error(result.error);
}
return result;
};
================================================
FILE: infra/testing/comlink/sw-interface.js
================================================
/*
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.
*/
importScripts('/__WORKBOX/comlink.js');
// TODO: Standardize on naming, and move over some of the legacy uses of
// postMessage() to this new approach.
// These are all the methods that will be called in the SW, but are exposed in
// the window context via Comlink.
const api = {
cachesKeys: () => {
return caches.keys();
},
clearAllCaches: async () => {
const keys = await caches.keys();
return Promise.all(keys.map((key) => caches.delete(key)));
},
doesDbExist: (dbName) => {
return new Promise((resolve) => {
const result = indexedDB.open(dbName);
result.onupgradeneeded = (event) => {
event.target.transaction.abort();
event.target.result.close();
resolve(false);
};
result.onsuccess = (event) => {
event.target.result.close();
resolve(true);
};
});
},
getObjectStoreEntries: (dbName, objStoreName) => {
return new Promise((resolve) => {
const result = indexedDB.open(dbName);
result.onsuccess = (event) => {
const db = event.target.result;
db
.transaction(objStoreName)
.objectStore(objStoreName)
.getAll().onsuccess = (event) => {
resolve(event.target.result);
};
};
});
},
cacheURLs: async (cacheName) => {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
return requests.map((request) => request.url);
},
getCachedResponseText: async (cacheName, url) => {
const cache = await caches.open(cacheName);
const response = await cache.match(url);
return response.text();
},
isNavigationPreloadSupported: async () => {
return workbox.navigationPreload.isSupported();
},
};
self.addEventListener('message', (event) => {
if (event.data instanceof MessagePort) {
Comlink.expose(api, event.data);
event.data.start();
}
});
================================================
FILE: infra/testing/comlink/window-interface.js
================================================
/*
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.
*/
function initComlink() {
const channel = new MessageChannel();
navigator.serviceWorker.controller.postMessage(channel.port2, [
channel.port2,
]);
window._runInSW = Comlink.wrap(channel.port1);
channel.port1.start();
}
const scriptEl = document.createElement('script');
scriptEl.src = '/__WORKBOX/comlink.js';
scriptEl.addEventListener('load', () => {
if (navigator.serviceWorker.controller) {
initComlink();
}
navigator.serviceWorker.addEventListener('controllerchange', initComlink);
});
document.body.appendChild(scriptEl);
================================================
FILE: infra/testing/confirm-directory-contains.js
================================================
/*
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.
*/
const expect = require('chai').expect;
const globby = require('globby');
const upath = require('upath');
module.exports = async (directory, expectedContents) => {
const relativeFiles = await globby('**', {cwd: directory});
const absoluteFilesWithNativeSeparator = relativeFiles.map((file) =>
upath.resolve(directory, file).replace(/\//g, upath.sep),
);
expect(absoluteFilesWithNativeSeparator).to.have.members(expectedContents);
};
================================================
FILE: infra/testing/env-it.js
================================================
/*
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.
*/
module.exports = {
devOnly: {
it: function (title, cb) {
// If the wrapped callback expects done, then we need to call it() with
// a function that expects done.
if (cb.length === 1) {
it(title, function (done) {
if (process.env.NODE_ENV === 'production') {
return this.skip();
}
return cb(done);
});
} else {
it(title, function () {
if (process.env.NODE_ENV === 'production') {
return this.skip();
}
return cb();
});
}
},
},
prodOnly: {
it: function (title, cb) {
// If the wrapped callback expects done, then we need to call it() with
// a function that expects done.
if (cb.length === 1) {
it(title, function (done) {
if (process.env.NODE_ENV !== 'production') {
return this.skip();
}
return cb(done);
});
} else {
it(title, function () {
if (process.env.NODE_ENV !== 'production') {
return this.skip();
}
return cb();
});
}
},
},
};
================================================
FILE: infra/testing/expectError.js
================================================
/*
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.
*/
const expect = require('chai').expect;
const logHelper = require('../utils/log-helper');
module.exports = async (func, errorName, finalCb) => {
let caughtError = null;
try {
const result = func();
if (result && result instanceof Promise) {
await result;
}
} catch (err) {
caughtError = err;
}
if (!caughtError) {
throw new Error('Expected error to be thrown but function ran correctly.');
}
if (caughtError.constructor.name !== 'WorkboxError') {
logHelper.warn(`Unexpected error thrown.`, caughtError);
}
expect(caughtError.constructor.name).to.equal('WorkboxError');
expect(caughtError.name).to.equal(errorName);
if (finalCb) {
return finalCb(caughtError);
}
};
================================================
FILE: infra/testing/generate-variant-tests.js
================================================
/*
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.
*/
/**
* @callback VariantCallback
* @param {Object} variant
*/
/**
* This is a helper function that will auto-generate mocha unit tests
* for various inputs.
*
* @param {string} itTitle This is the title that will be passed to the it()
* function. The variant will be added to the end of this title to help
* idenfity the failing test.
* @param {Array} variants This should be all the variations of the
* test you wish to generate.
* @param {VariantCallback} func This is the function that will be called, with a
* variant as the only argument. This function should perform the desired test.
*/
const generateVariantTests = (itTitle, variants, func) => {
variants.forEach((variant) => {
// We are using function() {} here and NOT ARROW FUNCTIONS
// to work with Mocha's binding for tests.
it(`${itTitle}. Variant: '${JSON.stringify(variant)}'`, function () {
// Use .call to get the correct `this` binding needed by mocha.
// eslint-disable-next-line no-invalid-this
return func.call(this, variant);
});
});
};
if (module.exports) {
module.exports = generateVariantTests;
} else {
self.__workbox.generateVariantTests = generateVariantTests;
}
================================================
FILE: infra/testing/helpers/compareResponses.mjs
================================================
/*
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.
*/
const compareResponses = async (first, second, shouldBeSame) => {
const firstBody = await first.clone().text();
const secondBody = await second.clone().text();
if (shouldBeSame) {
expect(firstBody).to.equal(secondBody);
} else {
expect(firstBody).to.not.equal(secondBody);
}
};
export {compareResponses};
================================================
FILE: infra/testing/helpers/extendable-event-utils.mjs
================================================
/*
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.
*/
const extendLifetimePromises = new WeakMap();
const eventResponses = new WeakMap();
export const eventDoneWaiting = async (event, {catchErrors = true} = {}) => {
const promises = extendLifetimePromises.get(event);
let promise;
while ((promise = promises.shift())) {
// Ignore errors by default;
if (catchErrors) {
promise = promise.catch((e) => e);
}
await promise;
}
};
export const spyOnEvent = (event) => {
const promises = [];
extendLifetimePromises.set(event, promises);
event.waitUntil = sinon.stub().callsFake((promise) => {
promises.push(promise);
});
if (event instanceof FetchEvent) {
event.respondWith = sinon.stub().callsFake((responseOrPromise) => {
const promise = Promise.resolve(responseOrPromise);
eventResponses.set(event, promise);
promises.push(promise);
// TODO(philipwalton): we cannot currently call the native
// `respondWith()` due to this bug in Firefox:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1538756
// FetchEvent.prototype.respondWith.call(event, responseOrPromise);
});
}
};
export const dispatchAndWaitUntilDone = async (event) => {
spyOnEvent(event);
self.dispatchEvent(event);
await eventDoneWaiting(event);
};
export const dispatchAndWaitForResponse = async (event) => {
await dispatchAndWaitUntilDone(event);
const response = await eventResponses.get(event);
return response;
};
================================================
FILE: infra/testing/helpers/generateOpaqueResponse.mjs
================================================
/*
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.
*/
// Cache a resonse value and clone it instead of re-fetching every time.
let response;
export const generateOpaqueResponse = async () => {
if (!response) {
response = await fetch('https://google.com', {mode: 'no-cors'});
}
return response.clone();
};
================================================
FILE: infra/testing/helpers/generateUniqueResponse.mjs
================================================
/*
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.
*/
let uid = 0;
export const generateUniqueResponse = (responseInit = {}) => {
return new Response(`${++uid}`, responseInit);
};
================================================
FILE: infra/testing/helpers/sleep.mjs
================================================
/*
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.
*/
/**
* Asynchronously waits for the passed number of milliseconds.
*
* @param {number} ms
* @return {Promise}
*/
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
================================================
FILE: infra/testing/server/cross-origin-server.js
================================================
/*
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.
*/
const express = require('express');
const PORT = 3010;
let app;
let server;
function initApp(staticDir) {
app = express();
app.use(express.static(staticDir));
}
function start(staticDir) {
if (!app) {
initApp(staticDir);
}
return new Promise((resolve, reject) => {
server = app.listen(PORT, (error) => {
if (error) {
reject(error);
} else {
resolve(`http://localhost:${PORT}`);
}
});
});
}
function stop() {
if (server) {
server.close();
}
}
module.exports = {
start,
stop,
};
================================================
FILE: infra/testing/server/index.js
================================================
/*
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.
*/
const bodyParser = require('body-parser');
const express = require('express');
const nunjucks = require('nunjucks');
const path = require('path');
const requireDir = require('require-dir');
const serveIndex = require('serve-index');
const logHelper = require('../../utils/log-helper');
const RequestCounter = require('./request-counter');
const PORT = 3004;
let app;
let requestCounters;
let server;
function initApp() {
app = express();
// Configure nunjucks to work with express routes.
nunjucks.configure('./', {express: app, noCache: true});
// Exposed the `.body` property on requests for application/json.
app.use(bodyParser.json());
requestCounters = new Set();
app.use((req, res, next) => {
for (const requestCounter of requestCounters) {
requestCounter.count(req);
}
next();
});
const routes = Object.values(requireDir('./routes'));
for (const {match, handler, method} of routes) {
app[method || 'get'](match, handler);
}
const staticDir = path.resolve(__dirname, '..', '..', '..');
app.use(express.static(staticDir), serveIndex(staticDir, {icons: true}));
}
function start() {
if (!app) {
initApp();
}
return new Promise((resolve, reject) => {
server = app.listen(PORT, (error) => {
if (error) {
reject(error);
} else {
logHelper.log(`The test server is running at ${getAddress()}`);
resolve();
}
});
});
}
function stop() {
if (server) {
server.close();
}
}
function getAddress() {
return `http://localhost:${PORT}`;
}
function startCountingRequests(headerValue) {
const requestCounter = new RequestCounter(headerValue);
requestCounters.add(requestCounter);
return requestCounter;
}
function stopCountingRequests(requestCounter) {
requestCounters.delete(requestCounter);
}
module.exports = {
getAddress,
start,
startCountingRequests,
stop,
stopCountingRequests,
};
================================================
FILE: infra/testing/server/request-counter.js
================================================
/*
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.
*/
class RequestCounter {
constructor(headerName) {
this.headerName = headerName;
this.headerCount = {};
this.urlCount = {};
}
count(req) {
if (this.headerName) {
const headerValue = req.get(this.headerName);
if (headerValue !== undefined) {
if (!(headerValue in this.headerCount)) {
this.headerCount[headerValue] = 0;
}
this.headerCount[headerValue]++;
}
}
const url = req.url;
if (!(url in this.urlCount)) {
this.urlCount[url] = 0;
}
this.urlCount[url]++;
}
getHeaderCount(headerValue) {
return this.headerCount[headerValue] || 0;
}
getURLCount(url) {
return this.urlCount[url] || 0;
}
}
module.exports = RequestCounter;
================================================
FILE: infra/testing/server/routes/build-file.js
================================================
/*
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.
*/
const path = require('path');
const {
outputFilenameToPkgMap,
} = require('../../../../gulp-tasks/utils/output-filename-to-package-map');
const match = '/__WORKBOX/buildFile/:packageFile';
async function handler(req, res) {
const {packageFile} = req.params;
const pkg = outputFilenameToPkgMap[packageFile.split('.')[0]];
const pkgDir = path.resolve('packages', pkg.name);
let file;
if (packageFile.includes('.')) {
file = path.join(pkgDir, 'build', packageFile);
} else {
const pkg = outputFilenameToPkgMap[packageFile];
// If the pkg.module or pkg.main references something in the build
// directory, use that. Otherwise base the build file on the pkg name.
if (pkg.module && pkg.module.startsWith('build/')) {
file = path.join(pkgDir, pkg.module);
} else if (pkg.main && pkg.main.startsWith('build/')) {
file = path.join(pkgDir, pkg.main);
} else {
file = path.join(pkgDir, 'build', `${pkg.name}.prod.js`);
}
// When not specifying a dev or prod build via the filename,
// we default to the value of NODE_ENV.
if (process.env.NODE_ENV !== 'production') {
file = file.replace('.prod.', '.dev.');
}
}
res.sendFile(file);
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/comlink.js
================================================
/*
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.
*/
const match = '/__WORKBOX/comlink.js';
async function handler(req, res) {
const comlinkMain = require.resolve('comlink');
res.sendFile(comlinkMain);
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/integration-html.js
================================================
/*
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.
*/
const path = require('path');
const match = '/*/integration.html';
async function handler(req, res) {
const filePath = path.join(__dirname, '..', 'static', 'integration.html');
res.sendFile(filePath);
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/sw-bundle.js
================================================
/*
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.
*/
const {getPackages} = require('../../../../gulp-tasks/utils/get-packages');
const {needsTranspile, queueTranspile} =
require('../../../../gulp-tasks/transpile-typescript').functions;
const {nodeResolve} = require('@rollup/plugin-node-resolve');
const {rollup} = require('rollup');
const commonjs = require('@rollup/plugin-commonjs');
const multiEntry = require('@rollup/plugin-multi-entry');
const replace = require('@rollup/plugin-replace');
const SW_NAMESPACES = getPackages({type: 'sw'}).map((pkg) => {
return pkg.workbox.browserNamespace;
});
const match = '/test/:package/*/sw-bundle.js';
const caches = {};
async function handler(req, res) {
const env = process.env.NODE_ENV || 'development';
const packageName = req.params.package;
res.set('Content-Type', 'text/javascript');
try {
// Ensure the TypeScript transpile step has completed first.
if (needsTranspile(packageName)) {
await queueTranspile(packageName);
}
// Allows you to selectively run tests by adding the `?test=` to the URL.
const testFilter = req.query.filter || '**/test-*.mjs';
const bundle = await rollup({
input: `./test/${packageName}/sw/` + testFilter,
plugins: [
multiEntry(),
nodeResolve({
moduleDirectories: ['packages', 'node_modules'],
}),
// TODO(philipwalton): some of our shared testing helpers use commonjs
// so we have to support this for the time being.
commonjs({
exclude: '*.mjs',
}),
replace({
preventAssignment: true,
delimiters: ['', ''],
values: {
'process.env.NODE_ENV': JSON.stringify(env),
'SW_NAMESPACES': JSON.stringify(SW_NAMESPACES),
'WORKBOX_CDN_ROOT_URL': '/__WORKBOX/buildFile',
},
}),
],
// Fail in the case of warning, so rebuilds work.
onwarn({loc, message}) {
if (loc) {
message = `${loc.file} (${loc.line}:${loc.column}) ${message}`;
}
throw new Error(message);
},
cache: caches[env],
});
// Update the cache so subsequent bundles are faster, and make sure it
// keep the dev/prod caches separate since the source files won't change
// between those builds, but the outputs will.
caches[env] = bundle.cache;
const {output} = await bundle.generate({format: 'iife'});
res.send(output[0].code);
} catch (error) {
res.status(400).send('');
console.error(error);
}
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/templates-update.js
================================================
/*
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.
*/
const templateData = require('../template-data');
// An endpoint to update template data from within tests. Any JSON in the
// POST body will be merged with the existing template data.
const match = '/__WORKBOX/updateTemplate';
async function handler(req, res) {
templateData.assign(req.body);
res.end();
}
module.exports = {
handler,
match,
method: 'post',
};
================================================
FILE: infra/testing/server/routes/templates.js
================================================
/*
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.
*/
const path = require('path');
const templateData = require('../template-data');
// Matches any URL ending in `.njk` and renders the file in the
// `../templates/*` directory as the response.
// NOTE: this allows you to serve a template file with any directory path,
// which is useful when dealing with service worker scope.
const match = /(\.[a-z]+)\.njk$/;
async function handler(req, res) {
const ext = req.params[0];
// Since templates can change between tests without the URL changing,
// we need to make sure the browser doesn't cache the response.
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
res.set('Expires', '0');
switch (ext) {
case '.js':
case '.mjs':
res.set('Content-Type', 'text/javascript');
break;
case '.html':
res.set('Content-Type', 'text/html');
break;
}
const file = path.join(__dirname, '..', 'templates', path.basename(req.path));
res.render(file, templateData.get());
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/test-sw.js
================================================
/*
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.
*/
const globby = require('globby');
const path = require('path');
const templateData = require('../template-data');
const match = '/test/:packageName/sw/';
async function handler(req, res) {
// See https://github.com/GoogleChrome/workbox/pull/2744#issuecomment-774138051
if (!req.path.endsWith('/')) {
return res.redirect(req.path + '/');
}
const {packageName} = req.params;
const testFilter = req.query.filter || '**/test-*.mjs';
const testFiles =
(await globby(`test/${packageName}/sw/${testFilter}`)) || [];
const testModules = testFiles.map((file) => '/' + file);
if (testFiles.length > 0) {
templateData.assign({packageName, testModules, testFilter});
res.set('Content-Type', 'text/html');
// Since templates can change between tests without the URL changing,
// we need to make sure the browser doesn't cache the response.
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
res.set('Expires', '0');
const file = path.join(__dirname, '..', 'templates', 'test-sw.html.njk');
res.render(file, templateData.get());
} else {
res.status(404).send(`No test files found for: ${testFilter}`);
}
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/test-window.js
================================================
/*
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.
*/
const globby = require('globby');
const path = require('path');
const templateData = require('../template-data');
const match = '/test/:packageName/window/';
async function handler(req, res) {
const {packageName} = req.params;
const testFilter = req.query.filter || '**/test-*.mjs';
const testFiles =
(await globby(`test/${packageName}/window/**/${testFilter}`)) || [];
const testModules = testFiles.map((file) => '/' + file);
templateData.assign({packageName, testModules, testFilter});
res.set('Content-Type', 'text/html');
// Since templates can change between tests without the URL changing,
// we need to make sure the browser doesn't cache the response.
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
res.set('Expires', '0');
const file = path.join(__dirname, '..', 'templates', 'test-window.html.njk');
res.render(file, templateData.get());
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/unique-etag.js
================================================
/*
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.
*/
const match = '/__WORKBOX/uniqueETag';
let counter = 0;
async function handler(req, res) {
res.set('ETag', ++counter);
res.send(`ETag is ${counter}.`);
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/routes/unique-value.js
================================================
/*
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.
*/
const match = '/__WORKBOX/uniqueValue';
let counter = 0;
async function handler(req, res) {
res.send(`Unique value is ${counter++}.`);
}
module.exports = {
handler,
match,
};
================================================
FILE: infra/testing/server/static/integration.html
================================================
Integration Test Harness
You need to manually register the service worker.
================================================
FILE: infra/testing/server/template-data.js
================================================
/*
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.
*/
// We have to use global variables instead of local variables because
// at the moment we're using `clear-require` to reset all modules between
// tests, which means all local variables get reset, but globals persist.
global.__uid = 0;
global.__templateData = {
ENV: process.env.NODE_ENV,
uniqueID: () => {
return ++global.__uid;
},
};
const get = () => {
return Object.assign({}, global.__templateData);
};
const assign = (newData) => {
Object.assign(global.__templateData, newData);
};
module.exports = {get, assign};
================================================
FILE: infra/testing/server/templates/integration.html.njk
================================================
{{ title }}
{{ body }}
================================================
FILE: infra/testing/server/templates/sw-clients-claim.js.njk
================================================
/*
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.
*/
// {{ version }}
addEventListener('install', (event) => event.waitUntil(skipWaiting()));
addEventListener('activate', (event) => event.waitUntil(
clients.claim().catch((error) => console.error(error))));
================================================
FILE: infra/testing/server/templates/sw-no-skip-waiting.js.njk
================================================
/*
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.
*/
// {{ version }}
================================================
FILE: infra/testing/server/templates/sw-script-version.js.njk
================================================
/*
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.
*/
// {{ version }}
addEventListener('install', (event) => event.waitUntil(skipWaiting()));
addEventListener('activate', (event) => event.waitUntil(clients.claim()));
addEventListener('message', async (event) => {
// Assert the type and meta are correct.
if (event.data.type === 'GET_VERSION' &&
event.data.meta === 'workbox-window') {
event.ports[0].postMessage('{{ version }}');
}
});
================================================
FILE: infra/testing/server/templates/sw-skip-waiting-deferred.js.njk
================================================
/*
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.
*/
// {{ version }}
addEventListener('install', (event) => {
const doneInstalling = new Promise((resolve) => {
setTimeout(() => {
skipWaiting();
resolve();
}, 500);
});
event.waitUntil(doneInstalling);
});
================================================
FILE: infra/testing/server/templates/sw-skip-waiting-on-message.js.njk
================================================
/*
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.
*/
// {{ version }}
addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
skipWaiting();
}
});
================================================
FILE: infra/testing/server/templates/sw-skip-waiting.js.njk
================================================
/*
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.
*/
// {{ version }}
addEventListener('install', (event) => event.waitUntil(skipWaiting()));
================================================
FILE: infra/testing/server/templates/sw-window-ready.js.njk
================================================
/*
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.
*/
// {{ version }}
addEventListener('install', (event) => event.waitUntil(skipWaiting()));
addEventListener('activate', (event) => event.waitUntil(clients.claim()));
addEventListener('message', async (event) => {
// Assert the type and meta are correct.
if (event.data.type === 'WINDOW_READY' &&
event.data.meta === 'workbox-window') {
const windows = await clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
for (const win of windows) {
win.postMessage({type: 'sw:message:ready'});
}
}
});
================================================
FILE: infra/testing/server/templates/test-sw-runner.js.njk
================================================
/*
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.
*/
importScripts(
'/node_modules/mocha/mocha.js',
'/node_modules/chai/chai.js',
'/node_modules/sinon/pkg/sinon-no-sourcemaps.js'
);
// Disable workbox logs in test mode.
self.__WB_DISABLE_DEV_LOGS = true;
self.expect = chai.expect;
// TODO(philipwalton): Move these globals back into imports once the
// conversion to run all unit tests in browsers is complete.
self.expectError = async (func, errorName, finalCb) => {
let caughtError = null;
try {
const result = func();
if (result && result instanceof Promise) {
await result;
}
} catch (err) {
caughtError = err;
}
if (!caughtError) {
throw new Error('Expected error to be thrown but function ran correctly.');
}
if (caughtError.constructor.name !== 'WorkboxError') {
console.warn(`Unexpected error thrown.`, caughtError);
}
expect(caughtError.constructor.name).to.equal('WorkboxError');
expect(caughtError.name).to.equal(errorName);
if (finalCb) {
return finalCb(caughtError);
}
};
self.waitUntil = async (fn, retries = 20, intervalMillis = 50) => {
for (let i = 0; i < retries; i++) {
const result = await fn();
if (result) {
return;
}
await new Promise((resolve) => setTimeout(resolve, intervalMillis));
}
throw new Error(`${fn} didn't return true after ${retries} retries.`);
};
mocha.setup({
ui: 'bdd',
timeout: 5000,
reporter: null,
});
addEventListener('install', (event) => {
const testsComplete = new Promise((resolve, reject) => {
const reports = [];
const runner = mocha.run();
runner.on('fail', (test, err) => {
const flattenTitles = (test) => {
const titles = [test.title];
while (test.parent.title) {
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse().join(' ');
};
reports.push({
name: flattenTitles(test),
result: false,
message: err.message,
stack: err.stack,
});
});
runner.on('end', async () => {
const results = runner.stats;
results.reports = reports;
const windows = await clients.matchAll({
type: 'window',
includeUncontrolled: true,
});
for (const win of windows) {
win.postMessage({
type: 'MOCHA_RESULTS',
payload: results,
});
}
// Fail installation if the tests don't pass.
if (results.failures) {
reject();
} else {
resolve();
}
});
});
event.waitUntil(testsComplete);
}, {once: true}); // Run once since `install` events are dispatched in tests.
importScripts('sw-bundle.js?filter={{ testFilter }}');
================================================
FILE: infra/testing/server/templates/test-sw.html.njk
================================================
Workbox SW Unit Tests
Service Worker Test Runner
Running tests for the following modules:
{% block tests %}
{% for testModule in testModules %}
{{ testModule }}"
{% endfor %}
{% endblock %}
Open the console to see progress...
================================================
FILE: infra/testing/server/templates/test-window.html.njk
================================================
Workbox Unit Tests
{% block tests %}
{% for testModule in testModules %}
{% endfor %}
{% endblock %}
================================================
FILE: infra/testing/validator/service-worker-runtime.js
================================================
/*
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.
*/
const assert = require('assert');
const chai = require('chai');
const chaiMatchPattern = require('chai-match-pattern');
const fse = require('fs-extra');
const makeServiceWorkerEnv = require('service-worker-mock');
const sinon = require('sinon');
const vm = require('vm');
chai.use(chaiMatchPattern);
const {expect} = chai;
// See https://github.com/chaijs/chai/issues/697
function stringifyFunctionsInArray(arr) {
return arr.map((item) =>
typeof item === 'function' ? item.toString() : item,
);
}
function setupSpiesAndContextForInjectManifest() {
const cacheableResponsePluginSpy = sinon.spy();
class CacheableResponsePlugin {
constructor(...args) {
cacheableResponsePluginSpy(...args);
}
}
const cacheExpirationPluginSpy = sinon.spy();
class CacheExpirationPlugin {
constructor(...args) {
cacheExpirationPluginSpy(...args);
}
}
const importScripts = sinon.spy();
const addEventListener = sinon.stub();
const workbox = {
cacheableResponse: {
CacheableResponsePlugin: CacheableResponsePlugin,
},
expiration: {
CacheExpirationPlugin: CacheExpirationPlugin,
},
googleAnalytics: {
initialize: sinon.spy(),
},
precaching: {
// To make testing easier, hardcode this fake URL return value.
getCacheKeyForURL: sinon.stub().returns('/urlWithCacheKey'),
precacheAndRoute: sinon.spy(),
cleanupOutdatedCaches: sinon.spy(),
},
navigationPreload: {
enable: sinon.spy(),
},
routing: {
registerNavigationRoute: sinon.spy(),
registerRoute: sinon.spy(),
},
core: {
clientsClaim: sinon.spy(),
setCacheNameDetails: sinon.spy(),
},
setConfig: sinon.spy(),
// To make testing easier, return the name of the strategy.
strategies: {
CacheFirst: sinon.stub().returns({name: 'CacheFirst'}),
NetworkFirst: sinon.stub().returns({name: 'NetworkFirst'}),
},
};
const context = Object.assign(
{
importScripts,
workbox,
},
makeServiceWorkerEnv(),
);
context.self.addEventListener = addEventListener;
context.self.skipWaiting = sinon.spy();
const methodsToSpies = {
importScripts,
cacheableResponsePlugin: cacheableResponsePluginSpy,
cleanupOutdatedCaches: workbox.precaching.cleanupOutdatedCaches,
cacheExpirationPlugin: cacheExpirationPluginSpy,
CacheFirst: workbox.strategies.CacheFirst,
clientsClaim: workbox.core.clientsClaim,
getCacheKeyForURL: workbox.precaching.getCacheKeyForURL,
googleAnalyticsInitialize: workbox.googleAnalytics.initialize,
NetworkFirst: workbox.strategies.NetworkFirst,
navigationPreloadEnable: workbox.navigationPreload.enable,
precacheAndRoute: workbox.precaching.precacheAndRoute,
registerNavigationRoute: workbox.routing.registerNavigationRoute,
registerRoute: workbox.routing.registerRoute,
setCacheNameDetails: workbox.core.setCacheNameDetails,
setConfig: workbox.setConfig,
skipWaiting: context.self.skipWaiting,
};
return {addEventListener, context, methodsToSpies};
}
function setupSpiesAndContextForGenerateSW() {
const addEventListener = sinon.spy();
const importScripts = sinon.spy();
const workboxContext = {
importScripts,
CacheFirst: sinon.stub().returns({name: 'CacheFirst'}),
clientsClaim: sinon.spy(),
createHandlerBoundToURL: sinon.stub().returns('/urlWithCacheKey'),
enable: sinon.spy(),
initialize: sinon.spy(),
NavigationRoute: sinon.stub().returns({name: 'NavigationRoute'}),
NetworkFirst: sinon.stub().returns({name: 'NetworkFirst'}),
BroadcastUpdatePlugin: sinon.spy(),
CacheableResponsePlugin: sinon.spy(),
ExpirationPlugin: sinon.spy(),
PrecacheFallbackPlugin: sinon.spy(),
precacheAndRoute: sinon.spy(),
registerRoute: sinon.spy(),
setCacheNameDetails: sinon.spy(),
skipWaiting: sinon.spy(),
};
const context = Object.assign(
{
importScripts,
define: (scripts, callback) => {
importScripts(...scripts);
callback(workboxContext);
},
},
makeServiceWorkerEnv(),
);
context.self.addEventListener = addEventListener;
context.self.skipWaiting = workboxContext.skipWaiting;
return {addEventListener, context, methodsToSpies: workboxContext};
}
function validateMethodCalls({methodsToSpies, expectedMethodCalls, context}) {
for (const [method, spy] of Object.entries(methodsToSpies)) {
if (spy.called) {
const args = spy.args.map((arg) =>
Array.isArray(arg) ? stringifyFunctionsInArray(arg) : arg,
);
expect(args, `while testing method calls for ${method}`).to.matchPattern(
expectedMethodCalls[method],
);
} else {
expect(
expectedMethodCalls[method],
`while testing method calls for ${method}`,
).to.be.undefined;
}
}
// Special validation for __WB_DISABLE_DEV_LOGS, which is a boolean
// assignment, so we can't stub it out.
if ('__WB_DISABLE_DEV_LOGS' in expectedMethodCalls) {
expect(context.self.__WB_DISABLE_DEV_LOGS).to.eql(
expectedMethodCalls.__WB_DISABLE_DEV_LOGS,
`__WB_DISABLE_DEV_LOGS`,
);
}
}
/**
* This is used in the service worker generation tests to validate core
* service worker functionality. While we don't fully emulate a real service
* worker runtime, we set up spies/stubs to listen for certain method calls,
* run the code in a VM sandbox, and then verify that the service worker
* made the expected method calls.
*
* If any of the expected method calls + parameter combinations were not made,
* this method will reject with a description of what failed.
*
* @param {string} [swFile]
* @param {string} [swString]
* @param {Object} expectedMethodCalls
* @return {Promise} Resolves if all of the expected method calls were made.
*/
module.exports = async ({
addEventListenerValidation,
entryPoint,
expectedMethodCalls,
swFile,
swString,
}) => {
assert(
(swFile || swString) && !(swFile && swString),
`Set swFile or swString, but not both.`,
);
if (swFile) {
swString = await fse.readFile(swFile, 'utf8');
}
const {addEventListener, context, methodsToSpies} =
entryPoint === 'injectManifest'
? setupSpiesAndContextForInjectManifest()
: setupSpiesAndContextForGenerateSW();
vm.runInNewContext(swString, context);
if (expectedMethodCalls) {
validateMethodCalls({methodsToSpies, expectedMethodCalls, context});
}
// Optionally check the usage of addEventListener().
if (addEventListenerValidation) {
addEventListenerValidation(addEventListener);
}
};
================================================
FILE: infra/testing/wait-until.js
================================================
/*
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.
*/
module.exports = async (fn, retries = 20, intervalMillis = 50) => {
for (let i = 0; i < retries; i++) {
const result = await fn();
if (result) {
return;
}
await new Promise((resolve) => setTimeout(resolve, intervalMillis));
}
throw new Error(`${fn} didn't return true after ${retries} retries.`);
};
================================================
FILE: infra/testing/webdriver/IframeManager.js
================================================
/*
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.
*/
const PREFIX = 'iframe-';
const waitUntil = require('../wait-until');
class Client {
constructor(driver, id) {
this._driver = driver;
this._id = id;
}
async executeAsyncScript(code) {
const value = await this._driver.executeAsyncScript(
(id, code, cb) => {
const iframe = document.querySelector(`#${id}`);
Promise.resolve(iframe.contentWindow.eval(code))
.then((value) => cb(value))
.catch((err) => cb(err.toString()));
},
this._id,
code,
);
return value;
}
async wait(code) {
await waitUntil(() => this.executeAsyncScript(code));
}
remove() {
this._driver.executeScript((id) => {
const el = document.querySelector(`#${id}`);
document.body.removeChild(el);
}, this._id);
}
}
/**
* Wraps methods in the underlying webdriver API to create, switch between,
* and close tabs in a browser.
*/
class IframeManager {
/**
* @param {WebDriver} driver
*
* @private
*/
constructor(driver) {
this._driver = driver;
this._clients = new Set();
}
async createIframeClient(url) {
const iframeId = await this._driver.executeAsyncScript(
(url, prefix, cb) => {
const el = document.createElement('iframe');
if (!('iframeCount' in window)) {
window.iframeCount = 1;
}
const id = `${prefix}${window.iframeCount++}`;
el.addEventListener('load', () => {
cb(id);
});
el.src = url;
el.id = id;
document.body.appendChild(el);
},
url,
PREFIX,
);
return new Client(this._driver, iframeId);
}
}
module.exports = {IframeManager};
================================================
FILE: infra/testing/webdriver/executeAsyncAndCatch.js
================================================
/*
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.
*/
// Store local references of these globals.
const {webdriver} = global.__workbox;
/**
* Executes the passed function (and args) async and logs any errors that
* occur. Errors are assumed to be passed to the callback as an object
* with the `error` property.
*
* @param {...*} args
* @return {*}
*/
const executeAsyncAndCatch = async (...args) => {
const result = await webdriver.executeAsyncScript(...args);
if (result && result.error) {
console.error(result.error);
throw new Error('Error executing async script');
}
return result;
};
module.exports = {executeAsyncAndCatch};
================================================
FILE: infra/testing/webdriver/runUnitTests.js
================================================
/*
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.
*/
const waitUntil = require('../wait-until');
// Store local references of these globals.
const {webdriver, server} = global.__workbox;
const runUnitTests = async (testPath) => {
await webdriver.get(server.getAddress() + testPath);
// Wait until the mocha tests are finished.
await waitUntil(
async () => {
return await webdriver.executeScript(() => self.mochaResults);
},
120,
500,
); // Retry for 60 seconds.
const results = await webdriver.executeScript(() => self.mochaResults);
if (results.failures > 0) {
console.log(`\n${results.failures} test failure(s):`);
for (const report of results.reports) {
console.log('');
console.log('Name : ', report.name);
console.log('Message : ', report.message);
console.log('Error : ', report.stack);
}
console.log('');
throw new Error('Unit tests failed, see logs above for details');
}
};
module.exports = {runUnitTests};
================================================
FILE: infra/testing/webdriver/unregisterAllSWs.js
================================================
/*
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.
*/
const {executeAsyncAndCatch} = require('./executeAsyncAndCatch');
/**
* Unregisters any active SWs so the next page load can start clean.
* Note: a new page load is needed before controlling SWs stop being active.
*/
const unregisterAllSWs = async () => {
await executeAsyncAndCatch(async (cb) => {
try {
const regs = await navigator.serviceWorker.getRegistrations();
for (const reg of regs) {
await reg.unregister();
}
cb();
} catch (error) {
cb({error: error.stack});
}
});
};
module.exports = {unregisterAllSWs};
================================================
FILE: infra/testing/webdriver/windowLoaded.js
================================================
/*
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.
*/
const {executeAsyncAndCatch} = require('./executeAsyncAndCatch');
/**
* Waits for the current window to load if it's not already loaded.
*/
const windowLoaded = async () => {
// Wait for the window to load, so the `Workbox` global is available.
await executeAsyncAndCatch(async (cb) => {
const loaded = () => {
if (!window.Workbox) {
cb({
error: `window.Workbox is undefined; location is ${location.href}`,
});
} else {
cb();
}
};
try {
if (document.readyState === 'complete') {
loaded();
} else {
addEventListener('load', () => loaded());
}
} catch (error) {
cb({error: error.stack});
}
});
};
module.exports = {windowLoaded};
================================================
FILE: infra/testing/webpack-build-check.js
================================================
/*
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.
*/
function joinMessages(errorsOrWarnings) {
if (errorsOrWarnings[0].message) {
return errorsOrWarnings.map((item) => item.message).join('\n');
} else {
return errorsOrWarnings.join('\n');
}
}
module.exports = (webpackError, stats) => {
if (webpackError) {
throw new Error(webpackError.message);
}
const statsJson = stats.toJson('verbose');
if (statsJson.errors.length > 0) {
throw new Error(joinMessages(statsJson.errors));
}
if (statsJson.warnings.length > 0) {
throw new Error(joinMessages(statsJson.warnings));
}
};
================================================
FILE: infra/type-overrides.d.ts
================================================
// TODO(philipwalton): remove these once this PR makes its way to a release:
// https://github.com/microsoft/TSJS-lib-generator/pull/701
interface IDBIndex {
openCursor(
range?: IDBValidKey | IDBKeyRange | null,
direction?: IDBCursorDirection,
): IDBRequest;
openKeyCursor(
range?: IDBValidKey | IDBKeyRange | null,
direction?: IDBCursorDirection,
): IDBRequest;
}
interface IDBObjectStore {
openCursor(
range?: IDBValidKey | IDBKeyRange | null,
direction?: IDBCursorDirection,
): IDBRequest;
openKeyCursor(
query?: IDBValidKey | IDBKeyRange | null,
direction?: IDBCursorDirection,
): IDBRequest;
}
// TODO(philipwalton): remove these once this PR makes its way to a release:
// https://github.com/microsoft/TSJS-lib-generator/pull/740
interface CacheStorage {
match(
request: RequestInfo,
options?: MultiCacheQueryOptions,
): Promise;
}
// TODO(philipwalton): remove these once this bug is fixed:
// https://github.com/microsoft/TypeScript/issues/32435
interface Headers {
[Symbol.iterator](): IterableIterator<[string, string]>;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*/
entries(): IterableIterator<[string, string]>;
/**
* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object.
*/
keys(): IterableIterator;
/**
* Returns an iterator allowing to go through all values of the key/value pairs contained in this object.
*/
values(): IterableIterator;
}
interface URLSearchParams {
[Symbol.iterator](): IterableIterator<[string, string]>;
/**
* Returns an array of key, value pairs for every entry in the search params.
*/
entries(): IterableIterator<[string, string]>;
/**
* Returns a list of keys in the search params.
*/
keys(): IterableIterator;
/**
* Returns a list of values in the search params.
*/
values(): IterableIterator;
}
================================================
FILE: infra/utils/AsyncDebounce.js
================================================
/*
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.
*/
class AsyncDebounce {
constructor(fn) {
this._fn = fn;
this._needsRecall = false;
}
call() {
if (!this._promise) {
/* eslint-disable no-async-promise-executor */
this._promise = new Promise(async (resolve) => {
do {
this._needsRecall = false;
await this._fn.call();
} while (this._needsRecall !== false);
this._promise = null;
resolve();
});
} else {
this._needsRecall = true;
}
return this._promise;
}
}
module.exports = {AsyncDebounce};
================================================
FILE: infra/utils/log-helper.js
================================================
/*
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.
*/
const chalk = require('chalk');
const prefix = function () {
return chalk.inverse(`[Workbox]:`);
};
module.exports = {
highlight: chalk.bgCyan,
debug: (...args) => {
console.log(prefix(), chalk.dim(...args));
},
log: (...args) => {
console.log(prefix(), ...args);
},
warn: (...args) => {
console.warn(prefix(), chalk.yellow(...args));
},
error: (...args) => {
console.error('\n');
console.error(prefix(), chalk.red(...args));
console.error('\n');
},
};
================================================
FILE: javascript.eslintrc.js
================================================
module.exports = {
extends: ['eslint:recommended', 'google'],
env: {
serviceworker: true,
browser: true,
node: true,
es6: true,
},
parserOptions: {
ecmaVersion: 2017,
sourceType: 'module',
},
globals: {
BroadcastChannel: false,
Comlink: false,
expect: true,
sinon: false,
SyncEvent: false,
workbox: false,
Workbox: true,
WorkboxSW: false,
},
rules: {
'indent': 0,
'jsdoc/check-types': 2,
'jsdoc/newline-after-description': 2,
'operator-linebreak': 0,
'space-before-function-paren': 0,
'max-len': [
2,
{
code: 80,
tabWidth: 2,
ignoreComments: true,
ignorePattern: '^\\s*import',
ignoreUrls: true,
},
],
},
plugins: ['jsdoc'],
settings: {
jsdoc: {
preferredTypes: {
object: 'Object',
},
},
},
overrides: [
{
files: ['test/**/*.{js,mjs}'],
env: {
mocha: true,
},
globals: {
expectError: false,
waitUntil: false,
SW_NAMESPACES: false,
},
rules: {
'max-len': 0,
'require-jsdoc': 0,
'valid-jsdoc': 0,
'no-invalid-this': 0,
},
},
{
files: [
'infra/testing/webdriver/executeAsyncAndCatch.js',
'infra/testing/webdriver/runUnitTests.js',
'infra/utils/log-helper.js',
'packages/workbox-core/_private/logger.mjs',
'packages/workbox-sw/_default.mjs',
'packages/workbox-cli/src/lib/logger.js',
'test/workbox-window/integration/test.js',
'test/workbox-window/window/test-Workbox.mjs',
],
rules: {
'no-console': 0,
},
},
{
files: ['infra/**/*.js'],
rules: {
'max-len': 0,
},
},
{
files: ['gulp-tasks/**/*.js', 'infra/**/*.js', 'test/**/*.js'],
rules: {
'camelcase': 0,
'require-jsdoc': 0,
'valid-jsdoc': 0,
},
},
{
files: ['infra/testing/**/*'],
env: {
mocha: true,
},
},
{
files: ['test/*/static/**/*.js'],
rules: {
'no-console': 0,
'no-unused-vars': 0,
'no-undef': 0,
},
},
{
files: ['packages/workbox-build/src/templates/**/*.js'],
rules: {
'max-len': 0,
},
},
{
files: ['packages/workbox-sw/**/*'],
globals: {
workbox: false,
},
},
{
files: ['infra/testing/env-it.js'],
rules: {
'no-invalid-this': 0,
},
},
{
files: [
'gulp-tasks/**/*.{mjs,js}',
'infra/**/*.{mjs,js}',
'packages/**/*.{mjs,js}',
'test/**/*.{mjs,js}',
],
plugins: ['header'],
rules: {
'header/header': [2, 'block', {pattern: 'Copyright \\d{4} Google LLC'}],
},
},
{
files: ['demos/**/*.js'],
rules: {
'no-console': 0,
},
},
],
// eslint can't parse some of these files.
ignorePatterns: ['**/wasm-project/**'],
};
================================================
FILE: jsdoc.conf
================================================
{
"source": {
"include": [
"packages"
],
"exclude": [
"packages/workbox-cli"
],
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "((^|\\/|\\\\)packages\\/.*\\/build(\\/|\\\\)|(^|\\/|\\\\)test(\\/|\\\\)|(^|\\/|\\\\)node_modules(\\/|\\\\)|(^|\\/|\\\\)demo(\\/|\\\\))"
},
"opts": {
"recurse": true
}
}
================================================
FILE: lerna.json
================================================
{
"lerna": "5.6.2",
"packages": ["packages/*"],
"version": "7.4.0"
}
================================================
FILE: package.json
================================================
{
"author": "Google's Web DevRel Team and Google's Aurora Team",
"bugs": {
"url": "https://github.com/GoogleChrome/workbox/issues"
},
"description": "Top-level scripts and dependencies for the workbox monorepo. Not meant to be published to npm.",
"devDependencies": {
"@babel/cli": "^7.24.1",
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@google-cloud/storage": "^5.20.5",
"@octokit/rest": "^19.0.13",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-multi-entry": "^7.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"@types/babel__core": "^7.20.5",
"@types/babel__preset-env": "^7.9.6",
"@types/common-tags": "^1.8.4",
"@types/eslint": "^8.56.10",
"@types/estree": "^1.0.5",
"@types/fs-extra": "^9.0.13",
"@types/lodash": "^4.17.0",
"@types/node": "^20.14.8",
"@types/stringify-object": "^3.3.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"acorn": "^8.11.3",
"babel-plugin-transform-async-to-promises": "^0.8.18",
"babylon": "^6.18.0",
"body-parser": "^1.20.2",
"bytes": "^3.1.2",
"camelcase": "^6.3.0",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.1",
"chai-match-pattern": "^1.3.0",
"chalk": "^4.1.2",
"clear-module": "^4.1.2",
"comlink": "^4.4.1",
"common-tags": "^1.8.2",
"copy-webpack-plugin": "^6.4.1",
"coveralls": "^3.1.1",
"del": "^5.1.0",
"depcheck": "^1.4.7",
"eslint": "^7.32.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-jsdoc": "^30.7.13",
"execa": "^4.1.0",
"express": "^4.19.2",
"fs-extra": "^9.1.0",
"glob": "^11.0.1",
"globby": "^11.1.0",
"gulp": "^4.0.2",
"gzip-size": "^5.1.1",
"html-webpack-plugin-v4": "npm:html-webpack-plugin@^4.5.0",
"html-webpack-plugin-v5": "npm:html-webpack-plugin@^5.0.0-alpha.14",
"husky": "^7.0.4",
"idb": "^7.1.1",
"jsdoc": "^3.6.11",
"jsdoc-baseline": "^0.1.5",
"lerna": "^5.6.2",
"lint-staged": "^11.2.6",
"memory-fs": "^0.5.0",
"minimist": "^1.2.8",
"mocha": "^9.2.2",
"module-alias": "^2.2.3",
"nunjucks": "^3.2.4",
"nyc": "^15.1.0",
"prettier": "^2.8.8",
"proxyquire": "^2.1.3",
"require-dir": "^1.2.0",
"rollup": "^4.53.3",
"selenium-assistant": "^6.1.1",
"semver": "^7.6.0",
"serve-index": "^1.9.1",
"service-worker-mock": "^1.9.3",
"sinon": "^9.0.3",
"tempy": "^0.7.1",
"type-fest": "^3.13.1",
"typescript": "^4.9.5",
"typescript-json-schema": "^0.63.0",
"upath": "^1.2.0",
"webpack-v4": "npm:webpack@^4.44.2",
"webpack-v5": "npm:webpack@^5.91.0",
"worker-plugin": "^5.0.1"
},
"engines": {
"node": ">=20.0.0"
},
"homepage": "https://github.com/GoogleChrome/workbox#readme",
"license": "MIT",
"lint-staged": {
"*": "prettier --ignore-unknown --write"
},
"name": "workbox",
"nyc": {
"exclude": [
"packages/workbox-core/models/messages/messages.mjs"
],
"extension": [
".mjs",
".js"
],
"include": [
"packages/**/*"
]
},
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/GoogleChrome/workbox.git"
},
"scripts": {
"build": "gulp build",
"gulp": "gulp",
"lint": "gulp lint",
"lint-staged": "lint-staged",
"prepare": "husky install",
"prettier": "prettier --ignore-unknown --write .",
"test_integration": "gulp test_integration",
"test_node": "gulp test_node",
"test_server": "gulp test_server",
"version": "gulp build && git add -A packages"
},
"version": ""
}
================================================
FILE: packages/workbox-background-sync/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-background-sync
================================================
FILE: packages/workbox-background-sync/package.json
================================================
{
"name": "workbox-background-sync",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "Queues failed requests and uses the Background Sync API to replay them when the network is available",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"background",
"sync",
"workbox-plugin"
],
"workbox": {
"browserNamespace": "workbox.backgroundSync",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"idb": "^7.0.1",
"workbox-core": "7.4.0"
}
}
================================================
FILE: packages/workbox-background-sync/src/BackgroundSyncPlugin.ts
================================================
/*
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 {WorkboxPlugin} from 'workbox-core/types.js';
import {Queue, QueueOptions} from './Queue.js';
import './_version.js';
/**
* A class implementing the `fetchDidFail` lifecycle callback. This makes it
* easier to add failed requests to a background sync Queue.
*
* @memberof workbox-background-sync
*/
class BackgroundSyncPlugin implements WorkboxPlugin {
private readonly _queue: Queue;
/**
* @param {string} name See the {@link workbox-background-sync.Queue}
* documentation for parameter details.
* @param {Object} [options] See the
* {@link workbox-background-sync.Queue} documentation for
* parameter details.
*/
constructor(name: string, options?: QueueOptions) {
this._queue = new Queue(name, options);
}
/**
* @param {Object} options
* @param {Request} options.request
* @private
*/
fetchDidFail: WorkboxPlugin['fetchDidFail'] = async ({request}) => {
await this._queue.pushRequest({request});
};
}
export {BackgroundSyncPlugin};
================================================
FILE: packages/workbox-background-sync/src/Queue.ts
================================================
/*
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 {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {logger} from 'workbox-core/_private/logger.js';
import {assert} from 'workbox-core/_private/assert.js';
import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
import {QueueStore} from './lib/QueueStore.js';
import {QueueStoreEntry, UnidentifiedQueueStoreEntry} from './lib/QueueDb.js';
import {StorableRequest} from './lib/StorableRequest.js';
import './_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
interface OnSyncCallbackOptions {
queue: Queue;
}
interface OnSyncCallback {
(options: OnSyncCallbackOptions): void | Promise;
}
export interface QueueOptions {
forceSyncFallback?: boolean;
maxRetentionTime?: number;
onSync?: OnSyncCallback;
}
interface QueueEntry {
request: Request;
timestamp?: number;
// We could use Record as a type but that would be a breaking
// change, better do it in next major release.
// eslint-disable-next-line @typescript-eslint/ban-types
metadata?: object;
}
const TAG_PREFIX = 'workbox-background-sync';
const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
const queueNames = new Set();
/**
* Converts a QueueStore entry into the format exposed by Queue. This entails
* converting the request data into a real request and omitting the `id` and
* `queueName` properties.
*
* @param {UnidentifiedQueueStoreEntry} queueStoreEntry
* @return {Queue}
* @private
*/
const convertEntry = (
queueStoreEntry: UnidentifiedQueueStoreEntry,
): QueueEntry => {
const queueEntry: QueueEntry = {
request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
timestamp: queueStoreEntry.timestamp,
};
if (queueStoreEntry.metadata) {
queueEntry.metadata = queueStoreEntry.metadata;
}
return queueEntry;
};
/**
* A class to manage storing failed requests in IndexedDB and retrying them
* later. All parts of the storing and replaying process are observable via
* callbacks.
*
* @memberof workbox-background-sync
*/
class Queue {
private readonly _name: string;
private readonly _onSync: OnSyncCallback;
private readonly _maxRetentionTime: number;
private readonly _queueStore: QueueStore;
private readonly _forceSyncFallback: boolean;
private _syncInProgress = false;
private _requestsAddedDuringSync = false;
/**
* Creates an instance of Queue with the given options
*
* @param {string} name The unique name for this queue. This name must be
* unique as it's used to register sync events and store requests
* in IndexedDB specific to this instance. An error will be thrown if
* a duplicate name is detected.
* @param {Object} [options]
* @param {Function} [options.onSync] A function that gets invoked whenever
* the 'sync' event fires. The function is invoked with an object
* containing the `queue` property (referencing this instance), and you
* can use the callback to customize the replay behavior of the queue.
* When not set the `replayRequests()` method is called.
* Note: if the replay fails after a sync event, make sure you throw an
* error, so the browser knows to retry the sync event later.
* @param {number} [options.maxRetentionTime=7 days] The amount of time (in
* minutes) a request may be retried. After this amount of time has
* passed, the request will be deleted from the queue.
* @param {boolean} [options.forceSyncFallback=false] If `true`, instead
* of attempting to use background sync events, always attempt to replay
* queued request at service worker startup. Most folks will not need
* this, unless you explicitly target a runtime like Electron that
* exposes the interfaces for background sync, but does not have a working
* implementation.
*/
constructor(
name: string,
{forceSyncFallback, onSync, maxRetentionTime}: QueueOptions = {},
) {
// Ensure the store name is not already being used
if (queueNames.has(name)) {
throw new WorkboxError('duplicate-queue-name', {name});
} else {
queueNames.add(name);
}
this._name = name;
this._onSync = onSync || this.replayRequests;
this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
this._forceSyncFallback = Boolean(forceSyncFallback);
this._queueStore = new QueueStore(this._name);
this._addSyncListener();
}
/**
* @return {string}
*/
get name(): string {
return this._name;
}
/**
* Stores the passed request in IndexedDB (with its timestamp and any
* metadata) at the end of the queue.
*
* @param {QueueEntry} entry
* @param {Request} entry.request The request to store in the queue.
* @param {Object} [entry.metadata] Any metadata you want associated with the
* stored request. When requests are replayed you'll have access to this
* metadata object in case you need to modify the request beforehand.
* @param {number} [entry.timestamp] The timestamp (Epoch time in
* milliseconds) when the request was first added to the queue. This is
* used along with `maxRetentionTime` to remove outdated requests. In
* general you don't need to set this value, as it's automatically set
* for you (defaulting to `Date.now()`), but you can update it if you
* don't want particular requests to expire.
*/
async pushRequest(entry: QueueEntry): Promise {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'pushRequest',
paramName: 'entry',
});
assert!.isInstance(entry.request, Request, {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'pushRequest',
paramName: 'entry.request',
});
}
await this._addRequest(entry, 'push');
}
/**
* Stores the passed request in IndexedDB (with its timestamp and any
* metadata) at the beginning of the queue.
*
* @param {QueueEntry} entry
* @param {Request} entry.request The request to store in the queue.
* @param {Object} [entry.metadata] Any metadata you want associated with the
* stored request. When requests are replayed you'll have access to this
* metadata object in case you need to modify the request beforehand.
* @param {number} [entry.timestamp] The timestamp (Epoch time in
* milliseconds) when the request was first added to the queue. This is
* used along with `maxRetentionTime` to remove outdated requests. In
* general you don't need to set this value, as it's automatically set
* for you (defaulting to `Date.now()`), but you can update it if you
* don't want particular requests to expire.
*/
async unshiftRequest(entry: QueueEntry): Promise {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'unshiftRequest',
paramName: 'entry',
});
assert!.isInstance(entry.request, Request, {
moduleName: 'workbox-background-sync',
className: 'Queue',
funcName: 'unshiftRequest',
paramName: 'entry.request',
});
}
await this._addRequest(entry, 'unshift');
}
/**
* Removes and returns the last request in the queue (along with its
* timestamp and any metadata). The returned object takes the form:
* `{request, timestamp, metadata}`.
*
* @return {Promise}
*/
async popRequest(): Promise {
return this._removeRequest('pop');
}
/**
* Removes and returns the first request in the queue (along with its
* timestamp and any metadata). The returned object takes the form:
* `{request, timestamp, metadata}`.
*
* @return {Promise}
*/
async shiftRequest(): Promise {
return this._removeRequest('shift');
}
/**
* Returns all the entries that have not expired (per `maxRetentionTime`).
* Any expired entries are removed from the queue.
*
* @return {Promise>}
*/
async getAll(): Promise> {
const allEntries = await this._queueStore.getAll();
const now = Date.now();
const unexpiredEntries = [];
for (const entry of allEntries) {
// Ignore requests older than maxRetentionTime. Call this function
// recursively until an unexpired request is found.
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
if (now - entry.timestamp > maxRetentionTimeInMs) {
await this._queueStore.deleteEntry(entry.id);
} else {
unexpiredEntries.push(convertEntry(entry));
}
}
return unexpiredEntries;
}
/**
* Returns the number of entries present in the queue.
* Note that expired entries (per `maxRetentionTime`) are also included in this count.
*
* @return {Promise}
*/
async size(): Promise {
return await this._queueStore.size();
}
/**
* Adds the entry to the QueueStore and registers for a sync event.
*
* @param {Object} entry
* @param {Request} entry.request
* @param {Object} [entry.metadata]
* @param {number} [entry.timestamp=Date.now()]
* @param {string} operation ('push' or 'unshift')
* @private
*/
async _addRequest(
{request, metadata, timestamp = Date.now()}: QueueEntry,
operation: 'push' | 'unshift',
): Promise {
const storableRequest = await StorableRequest.fromRequest(request.clone());
const entry: UnidentifiedQueueStoreEntry = {
requestData: storableRequest.toObject(),
timestamp,
};
// Only include metadata if it's present.
if (metadata) {
entry.metadata = metadata;
}
switch (operation) {
case 'push':
await this._queueStore.pushEntry(entry);
break;
case 'unshift':
await this._queueStore.unshiftEntry(entry);
break;
}
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Request for '${getFriendlyURL(request.url)}' has ` +
`been added to background sync queue '${this._name}'.`,
);
}
// Don't register for a sync if we're in the middle of a sync. Instead,
// we wait until the sync is complete and call register if
// `this._requestsAddedDuringSync` is true.
if (this._syncInProgress) {
this._requestsAddedDuringSync = true;
} else {
await this.registerSync();
}
}
/**
* Removes and returns the first or last (depending on `operation`) entry
* from the QueueStore that's not older than the `maxRetentionTime`.
*
* @param {string} operation ('pop' or 'shift')
* @return {Object|undefined}
* @private
*/
async _removeRequest(
operation: 'pop' | 'shift',
): Promise {
const now = Date.now();
let entry: QueueStoreEntry | undefined;
switch (operation) {
case 'pop':
entry = await this._queueStore.popEntry();
break;
case 'shift':
entry = await this._queueStore.shiftEntry();
break;
}
if (entry) {
// Ignore requests older than maxRetentionTime. Call this function
// recursively until an unexpired request is found.
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
if (now - entry.timestamp > maxRetentionTimeInMs) {
return this._removeRequest(operation);
}
return convertEntry(entry);
} else {
return undefined;
}
}
/**
* Loops through each request in the queue and attempts to re-fetch it.
* If any request fails to re-fetch, it's put back in the same position in
* the queue (which registers a retry for the next sync event).
*/
async replayRequests(): Promise {
let entry;
while ((entry = await this.shiftRequest())) {
try {
await fetch(entry.request.clone());
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Request for '${getFriendlyURL(entry.request.url)}' ` +
`has been replayed in queue '${this._name}'`,
);
}
} catch (error) {
await this.unshiftRequest(entry);
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Request for '${getFriendlyURL(entry.request.url)}' ` +
`failed to replay, putting it back in queue '${this._name}'`,
);
}
throw new WorkboxError('queue-replay-failed', {name: this._name});
}
}
if (process.env.NODE_ENV !== 'production') {
logger.log(
`All requests in queue '${this.name}' have successfully ` +
`replayed; the queue is now empty!`,
);
}
}
/**
* Registers a sync event with a tag unique to this instance.
*/
async registerSync(): Promise {
// See https://github.com/GoogleChrome/workbox/issues/2393
if ('sync' in self.registration && !this._forceSyncFallback) {
try {
await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
} catch (err) {
// This means the registration failed for some reason, possibly due to
// the user disabling it.
if (process.env.NODE_ENV !== 'production') {
logger.warn(
`Unable to register sync event for '${this._name}'.`,
err,
);
}
}
}
}
/**
* In sync-supporting browsers, this adds a listener for the sync event.
* In non-sync-supporting browsers, or if _forceSyncFallback is true, this
* will retry the queue on service worker startup.
*
* @private
*/
private _addSyncListener() {
// See https://github.com/GoogleChrome/workbox/issues/2393
if ('sync' in self.registration && !this._forceSyncFallback) {
self.addEventListener('sync', (event: SyncEvent) => {
if (event.tag === `${TAG_PREFIX}:${this._name}`) {
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Background sync for tag '${event.tag}' ` + `has been received`,
);
}
const syncComplete = async () => {
this._syncInProgress = true;
let syncError;
try {
await this._onSync({queue: this});
} catch (error) {
if (error instanceof Error) {
syncError = error;
// Rethrow the error. Note: the logic in the finally clause
// will run before this gets rethrown.
throw syncError;
}
} finally {
// New items may have been added to the queue during the sync,
// so we need to register for a new sync if that's happened...
// Unless there was an error during the sync, in which
// case the browser will automatically retry later, as long
// as `event.lastChance` is not true.
if (
this._requestsAddedDuringSync &&
!(syncError && !event.lastChance)
) {
await this.registerSync();
}
this._syncInProgress = false;
this._requestsAddedDuringSync = false;
}
};
event.waitUntil(syncComplete());
}
});
} else {
if (process.env.NODE_ENV !== 'production') {
logger.log(`Background sync replaying without background sync event`);
}
// If the browser doesn't support background sync, or the developer has
// opted-in to not using it, retry every time the service worker starts up
// as a fallback.
void this._onSync({queue: this});
}
}
/**
* Returns the set of queue names. This is primarily used to reset the list
* of queue names in tests.
*
* @return {Set}
*
* @private
*/
static get _queueNames(): Set {
return queueNames;
}
}
export {Queue};
================================================
FILE: packages/workbox-background-sync/src/QueueStore.ts
================================================
/*
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 './_version.js';
// This is a temporary workaround to expose something from ./lib/ via our
// top-level public API.
// TODO: In Workbox v7, move the actual code from ./lib/ to this file.
export {QueueStore} from './lib/QueueStore';
================================================
FILE: packages/workbox-background-sync/src/StorableRequest.ts
================================================
/*
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 './_version.js';
// This is a temporary workaround to expose something from ./lib/ via our
// top-level public API.
// TODO: In Workbox v7, move the actual code from ./lib/ to this file.
export {StorableRequest} from './lib/StorableRequest';
================================================
FILE: packages/workbox-background-sync/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:background-sync:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-background-sync/src/index.ts
================================================
/*
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 {BackgroundSyncPlugin} from './BackgroundSyncPlugin.js';
import {Queue, QueueOptions} from './Queue.js';
import {QueueStore} from './QueueStore.js';
import {StorableRequest} from './StorableRequest.js';
import './_version.js';
// See https://github.com/GoogleChrome/workbox/issues/2946
interface SyncManager {
getTags(): Promise;
register(tag: string): Promise;
}
declare global {
interface ServiceWorkerRegistration {
readonly sync: SyncManager;
}
interface SyncEvent extends ExtendableEvent {
readonly lastChance: boolean;
readonly tag: string;
}
interface ServiceWorkerGlobalScopeEventMap {
sync: SyncEvent;
}
}
/**
* @module workbox-background-sync
*/
export {BackgroundSyncPlugin, Queue, QueueOptions, QueueStore, StorableRequest};
================================================
FILE: packages/workbox-background-sync/src/lib/QueueDb.ts
================================================
/*
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 {openDB, DBSchema, IDBPDatabase} from 'idb';
import {RequestData} from './StorableRequest.js';
import '../_version.js';
interface QueueDBSchema extends DBSchema {
requests: {
key: number;
value: QueueStoreEntry;
indexes: {queueName: string};
};
}
const DB_VERSION = 3;
const DB_NAME = 'workbox-background-sync';
const REQUEST_OBJECT_STORE_NAME = 'requests';
const QUEUE_NAME_INDEX = 'queueName';
export interface UnidentifiedQueueStoreEntry {
requestData: RequestData;
timestamp: number;
id?: number;
queueName?: string;
// We could use Record as a type but that would be a breaking
// change, better do it in next major release.
// eslint-disable-next-line @typescript-eslint/ban-types
metadata?: object;
}
export interface QueueStoreEntry extends UnidentifiedQueueStoreEntry {
id: number;
}
/**
* A class to interact directly an IndexedDB created specifically to save and
* retrieve QueueStoreEntries. This class encapsulates all the schema details
* to store the representation of a Queue.
*
* @private
*/
export class QueueDb {
private _db: IDBPDatabase | null = null;
/**
* Add QueueStoreEntry to underlying db.
*
* @param {UnidentifiedQueueStoreEntry} entry
*/
async addEntry(entry: UnidentifiedQueueStoreEntry): Promise {
const db = await this.getDb();
const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, 'readwrite', {
durability: 'relaxed',
});
await tx.store.add(entry as QueueStoreEntry);
await tx.done;
}
/**
* Returns the first entry id in the ObjectStore.
*
* @return {number | undefined}
*/
async getFirstEntryId(): Promise {
const db = await this.getDb();
const cursor = await db
.transaction(REQUEST_OBJECT_STORE_NAME)
.store.openCursor();
return cursor?.value.id;
}
/**
* Get all the entries filtered by index
*
* @param queueName
* @return {Promise}
*/
async getAllEntriesByQueueName(
queueName: string,
): Promise {
const db = await this.getDb();
const results = await db.getAllFromIndex(
REQUEST_OBJECT_STORE_NAME,
QUEUE_NAME_INDEX,
IDBKeyRange.only(queueName),
);
return results ? results : new Array();
}
/**
* Returns the number of entries filtered by index
*
* @param queueName
* @return {Promise}
*/
async getEntryCountByQueueName(queueName: string): Promise {
const db = await this.getDb();
return db.countFromIndex(
REQUEST_OBJECT_STORE_NAME,
QUEUE_NAME_INDEX,
IDBKeyRange.only(queueName),
);
}
/**
* Deletes a single entry by id.
*
* @param {number} id the id of the entry to be deleted
*/
async deleteEntry(id: number): Promise {
const db = await this.getDb();
await db.delete(REQUEST_OBJECT_STORE_NAME, id);
}
/**
*
* @param queueName
* @returns {Promise}
*/
async getFirstEntryByQueueName(
queueName: string,
): Promise {
return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'next');
}
/**
*
* @param queueName
* @returns {Promise}
*/
async getLastEntryByQueueName(
queueName: string,
): Promise {
return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'prev');
}
/**
* Returns either the first or the last entries, depending on direction.
* Filtered by index.
*
* @param {IDBCursorDirection} direction
* @param {IDBKeyRange} query
* @return {Promise}
* @private
*/
async getEndEntryFromIndex(
query: IDBKeyRange,
direction: IDBCursorDirection,
): Promise {
const db = await this.getDb();
const cursor = await db
.transaction(REQUEST_OBJECT_STORE_NAME)
.store.index(QUEUE_NAME_INDEX)
.openCursor(query, direction);
return cursor?.value;
}
/**
* Returns an open connection to the database.
*
* @private
*/
private async getDb() {
if (!this._db) {
this._db = await openDB(DB_NAME, DB_VERSION, {
upgrade: this._upgradeDb,
});
}
return this._db;
}
/**
* Upgrades QueueDB
*
* @param {IDBPDatabase} db
* @param {number} oldVersion
* @private
*/
private _upgradeDb(db: IDBPDatabase, oldVersion: number) {
if (oldVersion > 0 && oldVersion < DB_VERSION) {
if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
}
}
const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
autoIncrement: true,
keyPath: 'id',
});
objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {unique: false});
}
}
================================================
FILE: packages/workbox-background-sync/src/lib/QueueStore.ts
================================================
/*
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 {assert} from 'workbox-core/_private/assert.js';
import {
UnidentifiedQueueStoreEntry,
QueueStoreEntry,
QueueDb,
} from './QueueDb.js';
import '../_version.js';
/**
* A class to manage storing requests from a Queue in IndexedDB,
* indexed by their queue name for easier access.
*
* Most developers will not need to access this class directly;
* it is exposed for advanced use cases.
*/
export class QueueStore {
private readonly _queueName: string;
private readonly _queueDb: QueueDb;
/**
* Associates this instance with a Queue instance, so entries added can be
* identified by their queue name.
*
* @param {string} queueName
*/
constructor(queueName: string) {
this._queueName = queueName;
this._queueDb = new QueueDb();
}
/**
* Append an entry last in the queue.
*
* @param {Object} entry
* @param {Object} entry.requestData
* @param {number} [entry.timestamp]
* @param {Object} [entry.metadata]
*/
async pushEntry(entry: UnidentifiedQueueStoreEntry): Promise {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'pushEntry',
paramName: 'entry',
});
assert!.isType(entry.requestData, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'pushEntry',
paramName: 'entry.requestData',
});
}
// Don't specify an ID since one is automatically generated.
delete entry.id;
entry.queueName = this._queueName;
await this._queueDb.addEntry(entry);
}
/**
* Prepend an entry first in the queue.
*
* @param {Object} entry
* @param {Object} entry.requestData
* @param {number} [entry.timestamp]
* @param {Object} [entry.metadata]
*/
async unshiftEntry(entry: UnidentifiedQueueStoreEntry): Promise {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(entry, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'unshiftEntry',
paramName: 'entry',
});
assert!.isType(entry.requestData, 'object', {
moduleName: 'workbox-background-sync',
className: 'QueueStore',
funcName: 'unshiftEntry',
paramName: 'entry.requestData',
});
}
const firstId = await this._queueDb.getFirstEntryId();
if (firstId) {
// Pick an ID one less than the lowest ID in the object store.
entry.id = firstId - 1;
} else {
// Otherwise let the auto-incrementor assign the ID.
delete entry.id;
}
entry.queueName = this._queueName;
await this._queueDb.addEntry(entry);
}
/**
* Removes and returns the last entry in the queue matching the `queueName`.
*
* @return {Promise}
*/
async popEntry(): Promise {
return this._removeEntry(
await this._queueDb.getLastEntryByQueueName(this._queueName),
);
}
/**
* Removes and returns the first entry in the queue matching the `queueName`.
*
* @return {Promise}
*/
async shiftEntry(): Promise {
return this._removeEntry(
await this._queueDb.getFirstEntryByQueueName(this._queueName),
);
}
/**
* Returns all entries in the store matching the `queueName`.
*
* @param {Object} options See {@link workbox-background-sync.Queue~getAll}
* @return {Promise>}
*/
async getAll(): Promise {
return await this._queueDb.getAllEntriesByQueueName(this._queueName);
}
/**
* Returns the number of entries in the store matching the `queueName`.
*
* @param {Object} options See {@link workbox-background-sync.Queue~size}
* @return {Promise}
*/
async size(): Promise {
return await this._queueDb.getEntryCountByQueueName(this._queueName);
}
/**
* Deletes the entry for the given ID.
*
* WARNING: this method does not ensure the deleted entry belongs to this
* queue (i.e. matches the `queueName`). But this limitation is acceptable
* as this class is not publicly exposed. An additional check would make
* this method slower than it needs to be.
*
* @param {number} id
*/
async deleteEntry(id: number): Promise {
await this._queueDb.deleteEntry(id);
}
/**
* Removes and returns the first or last entry in the queue (based on the
* `direction` argument) matching the `queueName`.
*
* @return {Promise}
* @private
*/
async _removeEntry(
entry?: QueueStoreEntry,
): Promise {
if (entry) {
await this.deleteEntry(entry.id);
}
return entry;
}
}
================================================
FILE: packages/workbox-background-sync/src/lib/StorableRequest.ts
================================================
/*
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 {assert} from 'workbox-core/_private/assert.js';
import {MapLikeObject} from 'workbox-core/types.js';
import '../_version.js';
type SerializableProperties =
| 'method'
| 'referrer'
| 'referrerPolicy'
| 'mode'
| 'credentials'
| 'cache'
| 'redirect'
| 'integrity'
| 'keepalive';
const serializableProperties: SerializableProperties[] = [
'method',
'referrer',
'referrerPolicy',
'mode',
'credentials',
'cache',
'redirect',
'integrity',
'keepalive',
];
export interface RequestData extends MapLikeObject {
url: string;
headers: MapLikeObject;
body?: ArrayBuffer;
}
/**
* A class to make it easier to serialize and de-serialize requests so they
* can be stored in IndexedDB.
*
* Most developers will not need to access this class directly;
* it is exposed for advanced use cases.
*/
class StorableRequest {
private readonly _requestData: RequestData;
/**
* Converts a Request object to a plain object that can be structured
* cloned or JSON-stringified.
*
* @param {Request} request
* @return {Promise}
*/
static async fromRequest(request: Request): Promise {
const requestData: RequestData = {
url: request.url,
headers: {},
};
// Set the body if present.
if (request.method !== 'GET') {
// Use ArrayBuffer to support non-text request bodies.
// NOTE: we can't use Blobs becuse Safari doesn't support storing
// Blobs in IndexedDB in some cases:
// https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457
requestData.body = await request.clone().arrayBuffer();
}
// Convert the headers from an iterable to an object.
for (const [key, value] of request.headers.entries()) {
requestData.headers[key] = value;
}
// Add all other serializable request properties
for (const prop of serializableProperties) {
if (request[prop] !== undefined) {
requestData[prop] = request[prop];
}
}
return new StorableRequest(requestData);
}
/**
* Accepts an object of request data that can be used to construct a
* `Request` but can also be stored in IndexedDB.
*
* @param {Object} requestData An object of request data that includes the
* `url` plus any relevant properties of
* [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}.
*/
constructor(requestData: RequestData) {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(requestData, 'object', {
moduleName: 'workbox-background-sync',
className: 'StorableRequest',
funcName: 'constructor',
paramName: 'requestData',
});
assert!.isType(requestData.url, 'string', {
moduleName: 'workbox-background-sync',
className: 'StorableRequest',
funcName: 'constructor',
paramName: 'requestData.url',
});
}
// If the request's mode is `navigate`, convert it to `same-origin` since
// navigation requests can't be constructed via script.
if (requestData['mode'] === 'navigate') {
requestData['mode'] = 'same-origin';
}
this._requestData = requestData;
}
/**
* Returns a deep clone of the instances `_requestData` object.
*
* @return {Object}
*/
toObject(): RequestData {
const requestData = Object.assign({}, this._requestData);
requestData.headers = Object.assign({}, this._requestData.headers);
if (requestData.body) {
requestData.body = requestData.body.slice(0);
}
return requestData;
}
/**
* Converts this instance to a Request.
*
* @return {Request}
*/
toRequest(): Request {
return new Request(this._requestData.url, this._requestData);
}
/**
* Creates and returns a deep clone of the instance.
*
* @return {StorableRequest}
*/
clone(): StorableRequest {
return new StorableRequest(this.toObject());
}
}
export {StorableRequest};
================================================
FILE: packages/workbox-background-sync/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"references": [{"path": "../workbox-core/"}]
}
================================================
FILE: packages/workbox-broadcast-update/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-broadcast-update
================================================
FILE: packages/workbox-broadcast-update/package.json
================================================
{
"name": "workbox-broadcast-update",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "A service worker helper library that uses the Broadcast Channel API to announce when a cached response has updated",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"workbox-plugin"
],
"workbox": {
"browserNamespace": "workbox.broadcastUpdate",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"workbox-core": "7.4.0"
}
}
================================================
FILE: packages/workbox-broadcast-update/src/BroadcastCacheUpdate.ts
================================================
/*
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 {assert} from 'workbox-core/_private/assert.js';
import {timeout} from 'workbox-core/_private/timeout.js';
import {resultingClientExists} from 'workbox-core/_private/resultingClientExists.js';
import {CacheDidUpdateCallbackParam} from 'workbox-core/types.js';
import {logger} from 'workbox-core/_private/logger.js';
import {responsesAreSame} from './responsesAreSame.js';
import {
CACHE_UPDATED_MESSAGE_META,
CACHE_UPDATED_MESSAGE_TYPE,
DEFAULT_HEADERS_TO_CHECK,
NOTIFY_ALL_CLIENTS,
} from './utils/constants.js';
import './_version.js';
// UA-sniff Safari: https://stackoverflow.com/questions/7944460/detect-safari-browser
// TODO(philipwalton): remove once this Safari bug fix has been released.
// https://bugs.webkit.org/show_bug.cgi?id=201169
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
export interface BroadcastCacheUpdateOptions {
headersToCheck?: string[];
generatePayload?: (
options: CacheDidUpdateCallbackParam,
) => Record;
notifyAllClients?: boolean;
}
/**
* Generates the default payload used in update messages. By default the
* payload includes the `cacheName` and `updatedURL` fields.
*
* @return Object
* @private
*/
function defaultPayloadGenerator(
data: CacheDidUpdateCallbackParam,
): Record {
return {
cacheName: data.cacheName,
updatedURL: data.request.url,
};
}
/**
* Uses the `postMessage()` API to inform any open windows/tabs when a cached
* response has been updated.
*
* For efficiency's sake, the underlying response bodies are not compared;
* only specific response headers are checked.
*
* @memberof workbox-broadcast-update
*/
class BroadcastCacheUpdate {
private readonly _headersToCheck: string[];
private readonly _generatePayload: (
options: CacheDidUpdateCallbackParam,
) => Record;
private readonly _notifyAllClients: boolean;
/**
* Construct a BroadcastCacheUpdate instance with a specific `channelName` to
* broadcast messages on
*
* @param {Object} [options]
* @param {Array} [options.headersToCheck=['content-length', 'etag', 'last-modified']]
* A list of headers that will be used to determine whether the responses
* differ.
* @param {string} [options.generatePayload] A function whose return value
* will be used as the `payload` field in any cache update messages sent
* to the window clients.
* @param {boolean} [options.notifyAllClients=true] If true (the default) then
* all open clients will receive a message. If false, then only the client
* that make the original request will be notified of the update.
*/
constructor({
generatePayload,
headersToCheck,
notifyAllClients,
}: BroadcastCacheUpdateOptions = {}) {
this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK;
this._generatePayload = generatePayload || defaultPayloadGenerator;
this._notifyAllClients = notifyAllClients ?? NOTIFY_ALL_CLIENTS;
}
/**
* Compares two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* and sends a message (via `postMessage()`) to all window clients if the
* responses differ. Neither of the Responses can be
* [opaque](https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque-responses).
*
* The message that's posted has the following format (where `payload` can
* be customized via the `generatePayload` option the instance is created
* with):
*
* ```
* {
* type: 'CACHE_UPDATED',
* meta: 'workbox-broadcast-update',
* payload: {
* cacheName: 'the-cache-name',
* updatedURL: 'https://example.com/'
* }
* }
* ```
*
* @param {Object} options
* @param {Response} [options.oldResponse] Cached response to compare.
* @param {Response} options.newResponse Possibly updated response to compare.
* @param {Request} options.request The request.
* @param {string} options.cacheName Name of the cache the responses belong
* to. This is included in the broadcast message.
* @param {Event} options.event event The event that triggered
* this possible cache update.
* @return {Promise} Resolves once the update is sent.
*/
async notifyIfUpdated(options: CacheDidUpdateCallbackParam): Promise {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(options.cacheName, 'string', {
moduleName: 'workbox-broadcast-update',
className: 'BroadcastCacheUpdate',
funcName: 'notifyIfUpdated',
paramName: 'cacheName',
});
assert!.isInstance(options.newResponse, Response, {
moduleName: 'workbox-broadcast-update',
className: 'BroadcastCacheUpdate',
funcName: 'notifyIfUpdated',
paramName: 'newResponse',
});
assert!.isInstance(options.request, Request, {
moduleName: 'workbox-broadcast-update',
className: 'BroadcastCacheUpdate',
funcName: 'notifyIfUpdated',
paramName: 'request',
});
}
// Without two responses there is nothing to compare.
if (!options.oldResponse) {
return;
}
if (
!responsesAreSame(
options.oldResponse,
options.newResponse,
this._headersToCheck,
)
) {
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Newer response found (and cached) for:`,
options.request.url,
);
}
const messageData = {
type: CACHE_UPDATED_MESSAGE_TYPE,
meta: CACHE_UPDATED_MESSAGE_META,
payload: this._generatePayload(options),
};
// For navigation requests, wait until the new window client exists
// before sending the message
if (options.request.mode === 'navigate') {
let resultingClientId: string | undefined;
if (options.event instanceof FetchEvent) {
resultingClientId = options.event.resultingClientId;
}
const resultingWin = await resultingClientExists(resultingClientId);
// Safari does not currently implement postMessage buffering and
// there's no good way to feature detect that, so to increase the
// chances of the message being delivered in Safari, we add a timeout.
// We also do this if `resultingClientExists()` didn't return a client,
// which means it timed out, so it's worth waiting a bit longer.
if (!resultingWin || isSafari) {
// 3500 is chosen because (according to CrUX data) 80% of mobile
// websites hit the DOMContentLoaded event in less than 3.5 seconds.
// And presumably sites implementing service worker are on the
// higher end of the performance spectrum.
await timeout(3500);
}
}
if (this._notifyAllClients) {
const windows = await self.clients.matchAll({type: 'window'});
for (const win of windows) {
win.postMessage(messageData);
}
} else {
// See https://github.com/GoogleChrome/workbox/issues/2895
if (options.event instanceof FetchEvent) {
const client = await self.clients.get(options.event.clientId);
client?.postMessage(messageData);
}
}
}
}
}
export {BroadcastCacheUpdate};
================================================
FILE: packages/workbox-broadcast-update/src/BroadcastUpdatePlugin.ts
================================================
/*
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 {dontWaitFor} from 'workbox-core/_private/dontWaitFor.js';
import {WorkboxPlugin} from 'workbox-core/types.js';
import {
BroadcastCacheUpdate,
BroadcastCacheUpdateOptions,
} from './BroadcastCacheUpdate.js';
import './_version.js';
/**
* This plugin will automatically broadcast a message whenever a cached response
* is updated.
*
* @memberof workbox-broadcast-update
*/
class BroadcastUpdatePlugin implements WorkboxPlugin {
private readonly _broadcastUpdate: BroadcastCacheUpdate;
/**
* Construct a {@link workbox-broadcast-update.BroadcastUpdate} instance with
* the passed options and calls its `notifyIfUpdated` method whenever the
* plugin's `cacheDidUpdate` callback is invoked.
*
* @param {Object} [options]
* @param {Array} [options.headersToCheck=['content-length', 'etag', 'last-modified']]
* A list of headers that will be used to determine whether the responses
* differ.
* @param {string} [options.generatePayload] A function whose return value
* will be used as the `payload` field in any cache update messages sent
* to the window clients.
*/
constructor(options?: BroadcastCacheUpdateOptions) {
this._broadcastUpdate = new BroadcastCacheUpdate(options);
}
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-sw` and `workbox-runtime-caching` handlers when an entry is
* added to a cache.
*
* @private
* @param {Object} options The input object to this function.
* @param {string} options.cacheName Name of the cache being updated.
* @param {Response} [options.oldResponse] The previous cached value, if any.
* @param {Response} options.newResponse The new value in the cache.
* @param {Request} options.request The request that triggered the update.
* @param {Request} options.event The event that triggered the update.
*/
cacheDidUpdate: WorkboxPlugin['cacheDidUpdate'] = async (options) => {
dontWaitFor(this._broadcastUpdate.notifyIfUpdated(options));
};
}
export {BroadcastUpdatePlugin};
================================================
FILE: packages/workbox-broadcast-update/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:broadcast-update:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-broadcast-update/src/index.ts
================================================
/*
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 {
BroadcastCacheUpdate,
BroadcastCacheUpdateOptions,
} from './BroadcastCacheUpdate.js';
import {BroadcastUpdatePlugin} from './BroadcastUpdatePlugin.js';
import {responsesAreSame} from './responsesAreSame.js';
import './_version.js';
/**
* @module workbox-broadcast-update
*/
export {
BroadcastCacheUpdate,
BroadcastCacheUpdateOptions,
BroadcastUpdatePlugin,
responsesAreSame,
};
================================================
FILE: packages/workbox-broadcast-update/src/responsesAreSame.ts
================================================
/*
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 {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {logger} from 'workbox-core/_private/logger.js';
import './_version.js';
/**
* Given two `Response's`, compares several header values to see if they are
* the same or not.
*
* @param {Response} firstResponse
* @param {Response} secondResponse
* @param {Array} headersToCheck
* @return {boolean}
*
* @memberof workbox-broadcast-update
*/
const responsesAreSame = (
firstResponse: Response,
secondResponse: Response,
headersToCheck: string[],
): boolean => {
if (process.env.NODE_ENV !== 'production') {
if (
!(firstResponse instanceof Response && secondResponse instanceof Response)
) {
throw new WorkboxError('invalid-responses-are-same-args');
}
}
const atLeastOneHeaderAvailable = headersToCheck.some((header) => {
return (
firstResponse.headers.has(header) && secondResponse.headers.has(header)
);
});
if (!atLeastOneHeaderAvailable) {
if (process.env.NODE_ENV !== 'production') {
logger.warn(
`Unable to determine where the response has been updated ` +
`because none of the headers that would be checked are present.`,
);
logger.debug(
`Attempting to compare the following: `,
firstResponse,
secondResponse,
headersToCheck,
);
}
// Just return true, indicating the that responses are the same, since we
// can't determine otherwise.
return true;
}
return headersToCheck.every((header) => {
const headerStateComparison =
firstResponse.headers.has(header) === secondResponse.headers.has(header);
const headerValueComparison =
firstResponse.headers.get(header) === secondResponse.headers.get(header);
return headerStateComparison && headerValueComparison;
});
};
export {responsesAreSame};
================================================
FILE: packages/workbox-broadcast-update/src/utils/constants.ts
================================================
/*
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 '../_version.js';
export const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';
export const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';
export const NOTIFY_ALL_CLIENTS = true;
export const DEFAULT_HEADERS_TO_CHECK: string[] = [
'content-length',
'etag',
'last-modified',
];
================================================
FILE: packages/workbox-broadcast-update/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"references": [{"path": "../workbox-core/"}]
}
================================================
FILE: packages/workbox-build/.ncurc.js
================================================
/*
Copyright 2020 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.
*/
// We use `npx npm-check-updates` to find updates to dependencies.
// Some dependencies have breaking changes that we can't resolve.
// This config file excludes those dependencies from the checks
// until we're able to remediate our code to deal with them.
module.exports = {
reject: [
// joi v16 is the last release to support Node v10:
// https://github.com/sideway/joi/issues/2262
'@hapi/joi',
],
};
================================================
FILE: packages/workbox-build/README.md
================================================
This module's documentation can be found at https://developer.chrome.com/docs/workbox/modules/workbox-build
================================================
FILE: packages/workbox-build/package.json
================================================
{
"name": "workbox-build",
"version": "7.4.0",
"description": "A module that integrates into your build process, helping you generate a manifest of local files that workbox-sw should precache.",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"caching",
"fetch requests",
"offline",
"file manifest"
],
"engines": {
"node": ">=20.0.0"
},
"author": "Google's Web DevRel Team and Google's Aurora Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/GoogleChrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.1",
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.11.0",
"@babel/runtime": "^7.11.2",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1",
"ajv": "^8.6.0",
"common-tags": "^1.8.0",
"fast-json-stable-stringify": "^2.1.0",
"fs-extra": "^9.0.1",
"glob": "^11.0.1",
"lodash": "^4.17.20",
"pretty-bytes": "^5.3.0",
"rollup": "^4.53.3",
"source-map": "^0.8.0-beta.0",
"stringify-object": "^3.3.0",
"strip-comments": "^2.0.1",
"tempy": "^0.6.0",
"upath": "^1.2.0",
"workbox-background-sync": "7.4.0",
"workbox-broadcast-update": "7.4.0",
"workbox-cacheable-response": "7.4.0",
"workbox-core": "7.4.0",
"workbox-expiration": "7.4.0",
"workbox-google-analytics": "7.4.0",
"workbox-navigation-preload": "7.4.0",
"workbox-precaching": "7.4.0",
"workbox-range-requests": "7.4.0",
"workbox-recipes": "7.4.0",
"workbox-routing": "7.4.0",
"workbox-strategies": "7.4.0",
"workbox-streams": "7.4.0",
"workbox-sw": "7.4.0",
"workbox-window": "7.4.0"
},
"main": "build/index.js",
"workbox": {
"packageType": "node_ts"
},
"types": "build/index.d.ts",
"devDependencies": {
"@types/node": "^20.14.8"
}
}
================================================
FILE: packages/workbox-build/src/_types.js
================================================
/*
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 './_version.mjs';
/**
* @typedef {Object} ManifestEntry
* @property {string} url The URL to the asset in the manifest.
* @property {string} revision The revision details for the file. This should be
* either a hash generated based on the file contents, or `null` if there is
* versioning already included in the URL.
* @property {string} [integrity] Integrity metadata that will be used when
* making the network request for the URL.
*
* @memberof module:workbox-build
*/
/**
* @typedef {Object} ManifestTransformResult
* @property {Array} manifest
* @property {Array|undefined} warnings
*
* @memberof module:workbox-build
*/
/**
* @typedef {Object} RuntimeCachingEntry
*
* @property {string|module:workbox-routing~handlerCallback} handler
* Either the name of one of the [built-in strategy classes]{@link module:workbox-strategies},
* or custom handler callback to use when the generated route matches.
*
* @property {string|RegExp|module:workbox-routing~matchCallback} urlPattern
* The value that will be passed to [`registerRoute()`]{@link module:workbox-routing.registerRoute},
* used to determine whether the generated route will match a given request.
*
* @property {string} [method='GET'] The
* [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) that
* will match the generated route.
*
* @property {Object} [options]
*
* @property {Object} [options.backgroundSync]
*
* @property {string} [options.backgroundSync.name] The `name` property to use
* when creating the
* [`BackgroundSyncPlugin`]{@link module:workbox-background-sync.BackgroundSyncPlugin}.
*
* @property {Object} [options.backgroundSync.options] The `options` property
* to use when creating the
* [`BackgroundSyncPlugin`]{@link module:workbox-background-sync.BackgroundSyncPlugin}.
*
* @property {Object} [options.broadcastUpdate]
*
* @property {string} [options.broadcastUpdate.channelName] The `channelName`
* property to use when creating the
* [`BroadcastCacheUpdatePlugin`]{@link module:workbox-broadcast-update.BroadcastUpdatePlugin}.
*
* @property {Object} [options.broadcastUpdate.options] The `options` property
* to use when creating the
* [`BroadcastCacheUpdatePlugin`]{@link module:workbox-broadcast-update.BroadcastUpdatePlugin}.
*
* @property {Object} [options.cacheableResponse]
*
* @property {Object} [options.cacheableResponse.headers] The `headers` property
* to use when creating the
* [`CacheableResponsePlugin`]{@link module:workbox-cacheable-response.CacheableResponsePlugin}.
*
* @property {Array} [options.cacheableResponse.statuses] `statuses`
* property to use when creating the
* [`CacheableResponsePlugin`]{@link module:workbox-cacheable-response.CacheableResponsePlugin}.
*
* @property {string} [options.cacheName] The `cacheName` to use when
* constructing one of the
* [Workbox strategy classes]{@link module:workbox-strategies}.
*
* @property {Object} [options.fetchOptions] The `fetchOptions` property value
* to use when constructing one of the
* [Workbox strategy classes]{@link module:workbox-strategies}.
*
* @property {Object} [options.expiration]
*
* @property {number} [options.expiration.maxAgeSeconds] The `maxAgeSeconds`
* property to use when creating the
* [`ExpirationPlugin`]{@link module:workbox-expiration.ExpirationPlugin}.
*
* @property {number} [options.expiration.maxEntries] The `maxEntries`
* property to use when creating the
* [`ExpirationPlugin`]{@link module:workbox-expiration.ExpirationPlugin}.
*
* @property {Object} [options.precacheFallback]
*
* @property {string} [options.precacheFallback.fallbackURL] The `fallbackURL`
* property to use when creating the
* [`PrecacheFallbackPlugin`]{@link module:workbox-precaching.PrecacheFallbackPlugin}.
*
* @property {boolean} [options.rangeRequests] Set to `true` to add the
* [`RangeRequestsPlugin`]{@link module:workbox-range-requests.RangeRequestsPlugin}
* for the strategy being configured.
*
* @property {Object} [options.matchOptions] The `matchOptions` property value
* to use when constructing one of the
* [Workbox strategy classes]{@link module:workbox-strategies}.
*
* @property {number} [options.networkTimeoutSeconds] The
* `networkTimeoutSeconds` property value to use when creating a
* [`NetworkFirst`]{@link module:workbox-strategies.NetworkFirst} strategy.
*
* @property {Array} [options.plugins]
* One or more [additional plugins](https://developers.google.com/web/tools/workbox/guides/using-plugins#custom_plugins)
* to apply to the handler. Useful when you want a plugin that doesn't have a
* "shortcut" configuration.
*
* @memberof module:workbox-build
*/
================================================
FILE: packages/workbox-build/src/cdn-details.json
================================================
{
"origin": "https://storage.googleapis.com",
"bucketName": "workbox-cdn",
"releasesDir": "releases",
"latestVersion": "7.4.0"
}
================================================
FILE: packages/workbox-build/src/generate-sw.ts
================================================
/*
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 upath from 'upath';
import {BuildResult, GetManifestOptions, GenerateSWOptions} from './types';
import {getFileManifestEntries} from './lib/get-file-manifest-entries';
import {rebasePath} from './lib/rebase-path';
import {validateGenerateSWOptions} from './lib/validate-options';
import {writeSWUsingDefaultTemplate} from './lib/write-sw-using-default-template';
/**
* This method creates a list of URLs to precache, referred to as a "precache
* manifest", based on the options you provide.
*
* It also takes in additional options that configures the service worker's
* behavior, like any `runtimeCaching` rules it should use.
*
* Based on the precache manifest and the additional configuration, it writes
* a ready-to-use service worker file to disk at `swDest`.
*
* ```
* // The following lists some common options; see the rest of the documentation
* // for the full set of options and defaults.
* const {count, size, warnings} = await generateSW({
* dontCacheBustURLsMatching: [new RegExp('...')],
* globDirectory: '...',
* globPatterns: ['...', '...'],
* maximumFileSizeToCacheInBytes: ...,
* navigateFallback: '...',
* runtimeCaching: [{
* // Routing via a matchCallback function:
* urlPattern: ({request, url}) => ...,
* handler: '...',
* options: {
* cacheName: '...',
* expiration: {
* maxEntries: ...,
* },
* },
* }, {
* // Routing via a RegExp:
* urlPattern: new RegExp('...'),
* handler: '...',
* options: {
* cacheName: '...',
* plugins: [..., ...],
* },
* }],
* skipWaiting: ...,
* swDest: '...',
* });
* ```
*
* @memberof workbox-build
*/
export async function generateSW(
config: GenerateSWOptions,
): Promise {
const options = validateGenerateSWOptions(config);
let entriesResult;
if (options.globDirectory) {
// Make sure we leave swDest out of the precache manifest.
options.globIgnores!.push(
rebasePath({
baseDirectory: options.globDirectory,
file: options.swDest,
}),
);
// If we create an extra external runtime file, ignore that, too.
// See https://rollupjs.org/guide/en/#outputchunkfilenames for naming.
if (!options.inlineWorkboxRuntime) {
const swDestDir = upath.dirname(options.swDest);
const workboxRuntimeFile = upath.join(swDestDir, 'workbox-*.js');
options.globIgnores!.push(
rebasePath({
baseDirectory: options.globDirectory,
file: workboxRuntimeFile,
}),
);
}
// We've previously asserted that options.globDirectory is set, so this
// should be a safe cast.
entriesResult = await getFileManifestEntries(options as GetManifestOptions);
} else {
entriesResult = {
count: 0,
manifestEntries: [],
size: 0,
warnings: [],
};
}
const filePaths = await writeSWUsingDefaultTemplate(
Object.assign(
{
manifestEntries: entriesResult.manifestEntries,
},
options,
),
);
return {
filePaths,
count: entriesResult.count,
size: entriesResult.size,
warnings: entriesResult.warnings,
};
}
================================================
FILE: packages/workbox-build/src/get-manifest.ts
================================================
/*
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 {getFileManifestEntries} from './lib/get-file-manifest-entries';
import {GetManifestOptions, GetManifestResult} from './types';
import {validateGetManifestOptions} from './lib/validate-options';
/**
* This method returns a list of URLs to precache, referred to as a "precache
* manifest", along with details about the number of entries and their size,
* based on the options you provide.
*
* ```
* // The following lists some common options; see the rest of the documentation
* // for the full set of options and defaults.
* const {count, manifestEntries, size, warnings} = await getManifest({
* dontCacheBustURLsMatching: [new RegExp('...')],
* globDirectory: '...',
* globPatterns: ['...', '...'],
* maximumFileSizeToCacheInBytes: ...,
* });
* ```
*
* @memberof workbox-build
*/
export async function getManifest(
config: GetManifestOptions,
): Promise {
const options = validateGetManifestOptions(config);
return await getFileManifestEntries(options);
}
================================================
FILE: packages/workbox-build/src/index.ts
================================================
/*
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 {copyWorkboxLibraries} from './lib/copy-workbox-libraries';
import {getModuleURL} from './lib/cdn-utils';
import {generateSW} from './generate-sw';
import {getManifest} from './get-manifest';
import {injectManifest} from './inject-manifest';
/**
* @module workbox-build
*/
export {
copyWorkboxLibraries,
generateSW,
getManifest,
getModuleURL,
injectManifest,
};
export * from './types';
================================================
FILE: packages/workbox-build/src/inject-manifest.ts
================================================
/*
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 {RawSourceMap} from 'source-map';
import assert from 'assert';
import fse from 'fs-extra';
import stringify from 'fast-json-stable-stringify';
import upath from 'upath';
import {BuildResult, InjectManifestOptions} from './types';
import {errors} from './lib/errors';
import {escapeRegExp} from './lib/escape-regexp';
import {getFileManifestEntries} from './lib/get-file-manifest-entries';
import {getSourceMapURL} from './lib/get-source-map-url';
import {rebasePath} from './lib/rebase-path';
import {replaceAndUpdateSourceMap} from './lib/replace-and-update-source-map';
import {translateURLToSourcemapPaths} from './lib/translate-url-to-sourcemap-paths';
import {validateInjectManifestOptions} from './lib/validate-options';
/**
* This method creates a list of URLs to precache, referred to as a "precache
* manifest", based on the options you provide.
*
* The manifest is injected into the `swSrc` file, and the placeholder string
* `injectionPoint` determines where in the file the manifest should go.
*
* The final service worker file, with the manifest injected, is written to
* disk at `swDest`.
*
* This method will not compile or bundle your `swSrc` file; it just handles
* injecting the manifest.
*
* ```
* // The following lists some common options; see the rest of the documentation
* // for the full set of options and defaults.
* const {count, size, warnings} = await injectManifest({
* dontCacheBustURLsMatching: [new RegExp('...')],
* globDirectory: '...',
* globPatterns: ['...', '...'],
* maximumFileSizeToCacheInBytes: ...,
* swDest: '...',
* swSrc: '...',
* });
* ```
*
* @memberof workbox-build
*/
export async function injectManifest(
config: InjectManifestOptions,
): Promise {
const options = validateInjectManifestOptions(config);
// Make sure we leave swSrc and swDest out of the precache manifest.
for (const file of [options.swSrc, options.swDest]) {
options.globIgnores!.push(
rebasePath({
file,
baseDirectory: options.globDirectory,
}),
);
}
const globalRegexp = new RegExp(escapeRegExp(options.injectionPoint!), 'g');
const {count, size, manifestEntries, warnings} = await getFileManifestEntries(
options,
);
let swFileContents: string;
try {
swFileContents = await fse.readFile(options.swSrc, 'utf8');
} catch (error) {
throw new Error(
`${errors['invalid-sw-src']} ${
error instanceof Error && error.message ? error.message : ''
}`,
);
}
const injectionResults = swFileContents.match(globalRegexp);
// See https://github.com/GoogleChrome/workbox/issues/2230
const injectionPoint = options.injectionPoint ? options.injectionPoint : '';
if (!injectionResults) {
if (upath.resolve(options.swSrc) === upath.resolve(options.swDest)) {
throw new Error(`${errors['same-src-and-dest']} ${injectionPoint}`);
}
throw new Error(`${errors['injection-point-not-found']} ${injectionPoint}`);
}
assert(
injectionResults.length === 1,
`${errors['multiple-injection-points']} ${injectionPoint}`,
);
const manifestString = stringify(manifestEntries);
const filesToWrite: {[key: string]: string} = {};
const url = getSourceMapURL(swFileContents);
// See https://github.com/GoogleChrome/workbox/issues/2957
const {destPath, srcPath, warning} = translateURLToSourcemapPaths(
url,
options.swSrc,
options.swDest,
);
if (warning) {
warnings.push(warning);
}
// If our swSrc file contains a sourcemap, we would invalidate that
// mapping if we just replaced injectionPoint with the stringified manifest.
// Instead, we need to update the swDest contents as well as the sourcemap
// (assuming it's a real file, not a data: URL) at the same time.
// See https://github.com/GoogleChrome/workbox/issues/2235
// and https://github.com/GoogleChrome/workbox/issues/2648
if (srcPath && destPath) {
const originalMap = (await fse.readJSON(srcPath, {
encoding: 'utf8',
})) as RawSourceMap;
const {map, source} = await replaceAndUpdateSourceMap({
originalMap,
jsFilename: upath.basename(options.swDest),
originalSource: swFileContents,
replaceString: manifestString,
searchString: options.injectionPoint!,
});
filesToWrite[options.swDest] = source;
filesToWrite[destPath] = map;
} else {
// If there's no sourcemap associated with swSrc, a simple string
// replacement will suffice.
filesToWrite[options.swDest] = swFileContents.replace(
globalRegexp,
manifestString,
);
}
for (const [file, contents] of Object.entries(filesToWrite)) {
try {
await fse.mkdirp(upath.dirname(file));
} catch (error: unknown) {
throw new Error(
errors['unable-to-make-sw-directory'] +
` '${error instanceof Error && error.message ? error.message : ''}'`,
);
}
await fse.writeFile(file, contents);
}
return {
count,
size,
warnings,
// Use upath.resolve() to make all the paths absolute.
filePaths: Object.keys(filesToWrite).map((f) => upath.resolve(f)),
};
}
================================================
FILE: packages/workbox-build/src/lib/additional-manifest-entries-transform.ts
================================================
/*
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): {
manifest: Array;
warnings: string[];
};
};
export function additionalManifestEntriesTransform(
additionalManifestEntries: Array,
): AdditionalManifestEntriesTransform {
return (manifest: Array) => {
const warnings: Array = [];
const stringEntries = new Set();
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,
};
};
}
================================================
FILE: packages/workbox-build/src/lib/bundle.ts
================================================
/*
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 '@trickfilm400/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 &
RequiredSWDestPartial & {unbundledCode: string}): Promise<
Array
> {
// 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;
plugins: Array;
} = {
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());
}
const bundle = await rollup(rollupConfig);
const outputOptions: {
sourcemap?: boolean;
format: 'es' | 'amd';
manualChunks?: (id: string) => string | undefined;
chunkFileNames?: string;
hashCharacters?: 'base64' | 'hex';
} = {
sourcemap,
// Using an external Workbox runtime requires 'amd'.
format: inlineWorkboxRuntime ? 'es' : 'amd',
};
if (!inlineWorkboxRuntime) {
outputOptions.manualChunks = (id) => {
return id.includes('workbox') ? 'workbox' : undefined;
};
outputOptions.hashCharacters = 'hex';
}
const {output} = await bundle.generate(outputOptions);
const files: Array = [];
for (const chunkOrAsset of output) {
if (chunkOrAsset.type === 'asset') {
if (!files.some((f) => f.name === chunkOrAsset.fileName)) {
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`;
if (!files.some((f) => f.name === sourceMapFile)) {
files.push({
name: sourceMapFile,
contents: chunkOrAsset.map.toString(),
});
}
}
if (!files.some((f) => f.name === chunkOrAsset.fileName)) {
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;
});
}
================================================
FILE: packages/workbox-build/src/lib/cdn-utils.ts
================================================
/*
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`;
}
================================================
FILE: packages/workbox-build/src/lib/copy-workbox-libraries.ts
================================================
/*
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} The name of the newly created directory.
*
* @alias workbox-build.copyWorkboxLibraries
*/
export async function copyWorkboxLibraries(
destDirectory: string,
): Promise {
// 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> = [];
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() : ''
}`,
);
}
}
================================================
FILE: packages/workbox-build/src/lib/errors.ts
================================================
/*
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: `,
};
================================================
FILE: packages/workbox-build/src/lib/escape-regexp.ts
================================================
/*
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, '\\$&');
}
================================================
FILE: packages/workbox-build/src/lib/get-composite-details.ts
================================================
/*
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 {
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,
};
}
================================================
FILE: packages/workbox-build/src/lib/get-file-details.ts
================================================
/*
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 & {
// This will only be called when globDirectory is not undefined.
globDirectory: string;
globPattern: string;
}): {
globbedFileDetails: Array;
warning: string;
} {
let globbedFiles: Array;
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 = [];
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};
}
================================================
FILE: packages/workbox-build/src/lib/get-file-hash.ts
================================================
/*
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 : ''}'`,
);
}
}
================================================
FILE: packages/workbox-build/src/lib/get-file-manifest-entries.ts
================================================
/*
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 {
const warnings: Array = [];
const allFileDetails = new Map();
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>(
(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} = {};
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;
}
================================================
FILE: packages/workbox-build/src/lib/get-file-size.ts
================================================
/*
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 : ''}'`,
);
}
}
================================================
FILE: packages/workbox-build/src/lib/get-source-map-url.ts
================================================
/*
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;
}
================================================
FILE: packages/workbox-build/src/lib/get-string-details.ts
================================================
/*
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,
};
}
================================================
FILE: packages/workbox-build/src/lib/get-string-hash.ts
================================================
/*
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');
}
================================================
FILE: packages/workbox-build/src/lib/maximum-size-transform.ts
================================================
/*
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 = [];
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};
};
}
================================================
FILE: packages/workbox-build/src/lib/modify-url-prefix-transform.ts
================================================
/*
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};
};
}
================================================
FILE: packages/workbox-build/src/lib/module-registry.ts
================================================
/*
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;
/**
* @private
*/
constructor() {
this._modulesUsed = new Map();
}
/**
* @return {Array} A list of all of the import statements that are
* needed for the modules being used.
* @private
*/
getImportStatements(): Array {
const workboxModuleImports: Array = [];
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;
}
}
================================================
FILE: packages/workbox-build/src/lib/no-revision-for-urls-matching-transform.ts
================================================
/*
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};
};
}
================================================
FILE: packages/workbox-build/src/lib/populate-sw-template.ts
================================================
/*
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}): 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)
: 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 : ''
}'`,
);
}
}
================================================
FILE: packages/workbox-build/src/lib/rebase-path.ts
================================================
/*
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;
}
================================================
FILE: packages/workbox-build/src/lib/replace-and-update-source-map.ts
================================================
/*
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,
};
}
================================================
FILE: packages/workbox-build/src/lib/runtime-caching-converter.ts
================================================
/*
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 = [];
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,
): Array {
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));
}
================================================
FILE: packages/workbox-build/src/lib/stringify-without-comments.ts
================================================
/*
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;
},
});
}
================================================
FILE: packages/workbox-build/src/lib/transform-manifest.ts
================================================
/*
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} 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}
* 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;
// When this is called by the webpack plugin, transformParam will be the
// current webpack compilation.
transformParam?: unknown;
}): Promise {
const allWarnings: Array = [];
// 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 = [];
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 =
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,
warnings: allWarnings,
};
}
================================================
FILE: packages/workbox-build/src/lib/translate-url-to-sourcemap-paths.ts
================================================
/*
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};
}
================================================
FILE: packages/workbox-build/src/lib/validate-options.ts
================================================
/*
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(
input: unknown,
methodName: MethodNames,
): [T, JSONSchemaType] {
// 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 = 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,
): void {
let validHandlers: Array = [];
/* 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(
input,
'GenerateSW',
);
ensureValidNavigationPreloadConfig(validatedOptions);
ensureValidCacheExpiration(validatedOptions);
ensureValidRuntimeCachingOrGlobDirectory(validatedOptions);
ensureValidStringHandler(validatedOptions, jsonSchema);
return validatedOptions;
}
export function validateGetManifestOptions(input: unknown): GetManifestOptions {
const [validatedOptions] = validate(input, 'GetManifest');
return validatedOptions;
}
export function validateInjectManifestOptions(
input: unknown,
): InjectManifestOptions {
const [validatedOptions] = validate(
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(
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(
inputWithExcludeDefault,
'WebpackInjectManifest',
);
return validatedOptions;
}
================================================
FILE: packages/workbox-build/src/lib/write-sw-using-default-template.ts
================================================
/*
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}): Promise<
Array
> {
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 = [];
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}'`);
}
}
================================================
FILE: packages/workbox-build/src/rollup-plugin-off-main-thread.d.ts
================================================
declare module '@trickfilm400/rollup-plugin-off-main-thread';
================================================
FILE: packages/workbox-build/src/schema/GenerateSWOptions.json
================================================
{
"additionalProperties": false,
"type": "object",
"properties": {
"additionalManifestEntries": {
"description": "A list of entries to be precached, in addition to any entries that are\ngenerated as part of the build configuration.",
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/ManifestEntry"
},
{
"type": "string"
}
]
}
},
"dontCacheBustURLsMatching": {
"description": "Assets that match this will be assumed to be uniquely versioned via their\nURL, and exempted from the normal HTTP cache-busting that's done when\npopulating the precache. While not required, it's recommended that if your\nexisting build process already inserts a `[hash]` value into each filename,\nyou provide a RegExp that will detect that, as it will reduce the bandwidth\nconsumed when precaching.",
"$ref": "#/definitions/RegExp"
},
"manifestTransforms": {
"description": "One or more functions which will be applied sequentially against the\ngenerated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are\nalso specified, their corresponding transformations will be applied first.",
"type": "array",
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"description": "This value can be used to determine the maximum size of files that will be\nprecached. This prevents you from inadvertently precaching very large files\nthat might have accidentally matched one of your patterns.",
"default": 2097152,
"type": "number"
},
"modifyURLPrefix": {
"description": "An object mapping string prefixes to replacement string values. This can be\nused to, e.g., remove or add a path prefix from a manifest entry if your\nweb hosting setup doesn't match your local filesystem setup. As an\nalternative with more flexibility, you can use the `manifestTransforms`\noption and provide a function that modifies the entries in the manifest\nusing whatever logic you provide.\n\nExample usage:\n\n```\n// Replace a '/dist/' prefix with '/', and also prepend\n// '/static' to every URL.\nmodifyURLPrefix: {\n '/dist/': '/',\n '': '/static',\n}\n```",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"globFollow": {
"description": "Determines whether or not symlinks are followed when generating the\nprecache manifest. For more information, see the definition of `follow` in\nthe `glob` [documentation](https://github.com/isaacs/node-glob#options).",
"default": true,
"type": "boolean"
},
"globIgnores": {
"description": "A set of patterns matching files to always exclude when generating the\nprecache manifest. For more information, see the definition of `ignore` in\nthe `glob` [documentation](https://github.com/isaacs/node-glob#options).",
"default": [
"**/node_modules/**/*"
],
"type": "array",
"items": {
"type": "string"
}
},
"globPatterns": {
"description": "Files matching any of these patterns will be included in the precache\nmanifest. For more information, see the\n[`glob` primer](https://github.com/isaacs/node-glob#glob-primer).",
"default": [
"**/*.{js,wasm,css,html}"
],
"type": "array",
"items": {
"type": "string"
}
},
"templatedURLs": {
"description": "If a URL is rendered based on some server-side logic, its contents may\ndepend on multiple files or on some other unique string value. The keys in\nthis object are server-rendered URLs. If the values are an array of\nstrings, they will be interpreted as `glob` patterns, and the contents of\nany files matching the patterns will be used to uniquely version the URL.\nIf used with a single string, it will be interpreted as unique versioning\ninformation that you've generated for a given URL.",
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
}
},
"babelPresetEnvTargets": {
"description": "The [targets](https://babeljs.io/docs/en/babel-preset-env#targets) to pass\nto `babel-preset-env` when transpiling the service worker bundle.",
"default": [
"chrome >= 56"
],
"type": "array",
"items": {
"type": "string"
}
},
"cacheId": {
"description": "An optional ID to be prepended to cache names. This is primarily useful for\nlocal development where multiple sites may be served from the same\n`http://localhost:port` origin.",
"type": [
"null",
"string"
]
},
"cleanupOutdatedCaches": {
"description": "Whether or not Workbox should attempt to identify and delete any precaches\ncreated by older, incompatible versions.",
"default": false,
"type": "boolean"
},
"clientsClaim": {
"description": "Whether or not the service worker should [start controlling](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim)\nany existing clients as soon as it activates.",
"default": false,
"type": "boolean"
},
"directoryIndex": {
"description": "If a navigation request for a URL ending in `/` fails to match a precached\nURL, this value will be appended to the URL and that will be checked for a\nprecache match. This should be set to what your web server is using for its\ndirectory index.",
"type": [
"null",
"string"
]
},
"disableDevLogs": {
"default": false,
"type": "boolean"
},
"ignoreURLParametersMatching": {
"description": "Any search parameter names that match against one of the RegExp in this\narray will be removed before looking for a precache match. This is useful\nif your users might request URLs that contain, for example, URL parameters\nused to track the source of the traffic. If not provided, the default value\nis `[/^utm_/, /^fbclid$/]`.",
"type": "array",
"items": {
"$ref": "#/definitions/RegExp"
}
},
"importScripts": {
"description": "A list of JavaScript files that should be passed to\n[`importScripts()`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts)\ninside the generated service worker file. This is useful when you want to\nlet Workbox create your top-level service worker file, but want to include\nsome additional code, such as a push event listener.",
"type": "array",
"items": {
"type": "string"
}
},
"inlineWorkboxRuntime": {
"description": "Whether the runtime code for the Workbox library should be included in the\ntop-level service worker, or split into a separate file that needs to be\ndeployed alongside the service worker. Keeping the runtime separate means\nthat users will not have to re-download the Workbox code each time your\ntop-level service worker changes.",
"default": false,
"type": "boolean"
},
"mode": {
"description": "If set to 'production', then an optimized service worker bundle that\nexcludes debugging info will be produced. If not explicitly configured\nhere, the `process.env.NODE_ENV` value will be used, and failing that, it\nwill fall back to `'production'`.",
"default": "production",
"type": [
"null",
"string"
]
},
"navigateFallback": {
"description": "If specified, all\n[navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests)\nfor URLs that aren't precached will be fulfilled with the HTML at the URL\nprovided. You must pass in the URL of an HTML document that is listed in\nyour precache manifest. This is meant to be used in a Single Page App\nscenario, in which you want all navigations to use common\n[App Shell HTML](https://developers.google.com/web/fundamentals/architecture/app-shell).",
"default": null,
"type": [
"null",
"string"
]
},
"navigateFallbackAllowlist": {
"description": "An optional array of regular expressions that restricts which URLs the\nconfigured `navigateFallback` behavior applies to. This is useful if only a\nsubset of your site's URLs should be treated as being part of a\n[Single Page App](https://en.wikipedia.org/wiki/Single-page_application).\nIf both `navigateFallbackDenylist` and `navigateFallbackAllowlist` are\nconfigured, the denylist takes precedent.\n\n*Note*: These RegExps may be evaluated against every destination URL during\na navigation. Avoid using\n[complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),\nor else your users may see delays when navigating your site.",
"type": "array",
"items": {
"$ref": "#/definitions/RegExp"
}
},
"navigateFallbackDenylist": {
"description": "An optional array of regular expressions that restricts which URLs the\nconfigured `navigateFallback` behavior applies to. This is useful if only a\nsubset of your site's URLs should be treated as being part of a\n[Single Page App](https://en.wikipedia.org/wiki/Single-page_application).\nIf both `navigateFallbackDenylist` and `navigateFallbackAllowlist` are\nconfigured, the denylist takes precedence.\n\n*Note*: These RegExps may be evaluated against every destination URL during\na navigation. Avoid using\n[complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),\nor else your users may see delays when navigating your site.",
"type": "array",
"items": {
"$ref": "#/definitions/RegExp"
}
},
"navigationPreload": {
"description": "Whether or not to enable\n[navigation preload](https://developers.google.com/web/tools/workbox/modules/workbox-navigation-preload)\nin the generated service worker. When set to true, you must also use\n`runtimeCaching` to set up an appropriate response strategy that will match\nnavigation requests, and make use of the preloaded response.",
"default": false,
"type": "boolean"
},
"offlineGoogleAnalytics": {
"description": "Controls whether or not to include support for\n[offline Google Analytics](https://developers.google.com/web/tools/workbox/guides/enable-offline-analytics).\nWhen `true`, the call to `workbox-google-analytics`'s `initialize()` will\nbe added to your generated service worker. When set to an `Object`, that\nobject will be passed in to the `initialize()` call, allowing you to\ncustomize the behavior.",
"default": false,
"anyOf": [
{
"$ref": "#/definitions/GoogleAnalyticsInitializeOptions"
},
{
"type": "boolean"
}
]
},
"runtimeCaching": {
"description": "When using Workbox's build tools to generate your service worker, you can\nspecify one or more runtime caching configurations. These are then\ntranslated to {@link workbox-routing.registerRoute} calls using the match\nand handler configuration you define.\n\nFor all of the options, see the {@link workbox-build.RuntimeCaching}\ndocumentation. The example below shows a typical configuration, with two\nruntime routes defined:",
"type": "array",
"items": {
"$ref": "#/definitions/RuntimeCaching"
}
},
"skipWaiting": {
"description": "Whether to add an unconditional call to [`skipWaiting()`](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#skip_the_waiting_phase)\nto the generated service worker. If `false`, then a `message` listener will\nbe added instead, allowing client pages to trigger `skipWaiting()` by\ncalling `postMessage({type: 'SKIP_WAITING'})` on a waiting service worker.",
"default": false,
"type": "boolean"
},
"sourcemap": {
"description": "Whether to create a sourcemap for the generated service worker files.",
"default": true,
"type": "boolean"
},
"swDest": {
"description": "The path and filename of the service worker file that will be created by\nthe build process, relative to the current working directory. It must end\nin '.js'.",
"type": "string"
},
"globDirectory": {
"description": "The local directory you wish to match `globPatterns` against. The path is\nrelative to the current directory.",
"type": "string"
}
},
"required": [
"swDest"
],
"definitions": {
"ManifestEntry": {
"type": "object",
"properties": {
"integrity": {
"type": "string"
},
"revision": {
"type": [
"null",
"string"
]
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"revision",
"url"
]
},
"RegExp": {
"type": "object",
"properties": {
"source": {
"type": "string"
},
"global": {
"type": "boolean"
},
"ignoreCase": {
"type": "boolean"
},
"multiline": {
"type": "boolean"
},
"lastIndex": {
"type": "number"
},
"flags": {
"type": "string"
},
"sticky": {
"type": "boolean"
},
"unicode": {
"type": "boolean"
},
"dotAll": {
"type": "boolean"
},
"hasIndices": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"dotAll",
"flags",
"global",
"hasIndices",
"ignoreCase",
"lastIndex",
"multiline",
"source",
"sticky",
"unicode"
]
},
"GoogleAnalyticsInitializeOptions": {
"type": "object",
"properties": {
"cacheName": {
"type": "string"
},
"parameterOverrides": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"hitFilter": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
"RuntimeCaching": {
"type": "object",
"properties": {
"handler": {
"description": "This determines how the runtime route will generate a response.\nTo use one of the built-in {@link workbox-strategies}, provide its name,\nlike `'NetworkFirst'`.\nAlternatively, this can be a {@link workbox-core.RouteHandler} callback\nfunction with custom response logic.",
"anyOf": [
{
"$ref": "#/definitions/RouteHandlerCallback"
},
{
"$ref": "#/definitions/RouteHandlerObject"
},
{
"enum": [
"CacheFirst",
"CacheOnly",
"NetworkFirst",
"NetworkOnly",
"StaleWhileRevalidate"
],
"type": "string"
}
]
},
"method": {
"description": "The HTTP method to match against. The default value of `'GET'` is normally\nsufficient, unless you explicitly need to match `'POST'`, `'PUT'`, or\nanother type of request.",
"default": "GET",
"enum": [
"DELETE",
"GET",
"HEAD",
"PATCH",
"POST",
"PUT"
],
"type": "string"
},
"options": {
"type": "object",
"properties": {
"backgroundSync": {
"description": "Configuring this will add a\n{@link workbox-background-sync.BackgroundSyncPlugin} instance to the\n{@link workbox-strategies} configured in `handler`.",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"options": {
"$ref": "#/definitions/QueueOptions"
}
},
"additionalProperties": false,
"required": [
"name"
]
},
"broadcastUpdate": {
"description": "Configuring this will add a\n{@link workbox-broadcast-update.BroadcastUpdatePlugin} instance to the\n{@link workbox-strategies} configured in `handler`.",
"type": "object",
"properties": {
"channelName": {
"type": "string"
},
"options": {
"$ref": "#/definitions/BroadcastCacheUpdateOptions"
}
},
"additionalProperties": false,
"required": [
"options"
]
},
"cacheableResponse": {
"description": "Configuring this will add a\n{@link workbox-cacheable-response.CacheableResponsePlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/CacheableResponseOptions"
},
"cacheName": {
"description": "If provided, this will set the `cacheName` property of the\n{@link workbox-strategies} configured in `handler`.",
"type": [
"null",
"string"
]
},
"expiration": {
"description": "Configuring this will add a\n{@link workbox-expiration.ExpirationPlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/ExpirationPluginOptions"
},
"networkTimeoutSeconds": {
"description": "If provided, this will set the `networkTimeoutSeconds` property of the\n{@link workbox-strategies} configured in `handler`. Note that only\n`'NetworkFirst'` and `'NetworkOnly'` support `networkTimeoutSeconds`.",
"type": "number"
},
"plugins": {
"description": "Configuring this allows the use of one or more Workbox plugins that\ndon't have \"shortcut\" options (like `expiration` for\n{@link workbox-expiration.ExpirationPlugin}). The plugins provided here\nwill be added to the {@link workbox-strategies} configured in `handler`.",
"type": "array",
"items": {
"$ref": "#/definitions/WorkboxPlugin"
}
},
"precacheFallback": {
"description": "Configuring this will add a\n{@link workbox-precaching.PrecacheFallbackPlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"type": "object",
"properties": {
"fallbackURL": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"fallbackURL"
]
},
"rangeRequests": {
"description": "Enabling this will add a\n{@link workbox-range-requests.RangeRequestsPlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"type": "boolean"
},
"fetchOptions": {
"description": "Configuring this will pass along the `fetchOptions` value to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/RequestInit"
},
"matchOptions": {
"description": "Configuring this will pass along the `matchOptions` value to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/CacheQueryOptions"
}
},
"additionalProperties": false
},
"urlPattern": {
"description": "This match criteria determines whether the configured handler will\ngenerate a response for any requests that don't match one of the precached\nURLs. If multiple `RuntimeCaching` routes are defined, then the first one\nwhose `urlPattern` matches will be the one that responds.\n\nThis value directly maps to the first parameter passed to\n{@link workbox-routing.registerRoute}. It's recommended to use a\n{@link workbox-core.RouteMatchCallback} function for greatest flexibility.",
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"$ref": "#/definitions/RouteMatchCallback"
},
{
"type": "string"
}
]
}
},
"additionalProperties": false,
"required": [
"handler",
"urlPattern"
]
},
"RouteHandlerCallback": {},
"RouteHandlerObject": {
"description": "An object with a `handle` method of type `RouteHandlerCallback`.\n\nA `Route` object can be created with either an `RouteHandlerCallback`\nfunction or this `RouteHandler` object. The benefit of the `RouteHandler`\nis it can be extended (as is done by the `workbox-strategies` package).",
"type": "object",
"properties": {
"handle": {
"$ref": "#/definitions/RouteHandlerCallback"
}
},
"additionalProperties": false,
"required": [
"handle"
]
},
"QueueOptions": {
"type": "object",
"properties": {
"forceSyncFallback": {
"type": "boolean"
},
"maxRetentionTime": {
"type": "number"
},
"onSync": {
"$ref": "#/definitions/OnSyncCallback"
}
},
"additionalProperties": false
},
"OnSyncCallback": {},
"BroadcastCacheUpdateOptions": {
"type": "object",
"properties": {
"headersToCheck": {
"type": "array",
"items": {
"type": "string"
}
},
"generatePayload": {
"type": "object",
"additionalProperties": false
},
"notifyAllClients": {
"type": "boolean"
}
},
"additionalProperties": false
},
"CacheableResponseOptions": {
"type": "object",
"properties": {
"statuses": {
"type": "array",
"items": {
"type": "number"
}
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"additionalProperties": false
},
"ExpirationPluginOptions": {
"type": "object",
"properties": {
"maxEntries": {
"type": "number"
},
"maxAgeSeconds": {
"type": "number"
},
"matchOptions": {
"$ref": "#/definitions/CacheQueryOptions"
},
"purgeOnQuotaError": {
"type": "boolean"
}
},
"additionalProperties": false
},
"CacheQueryOptions": {
"type": "object",
"properties": {
"ignoreMethod": {
"type": "boolean"
},
"ignoreSearch": {
"type": "boolean"
},
"ignoreVary": {
"type": "boolean"
}
},
"additionalProperties": false
},
"WorkboxPlugin": {
"description": "An object with optional lifecycle callback properties for the fetch and\ncache operations.",
"type": "object",
"properties": {
"cacheDidUpdate": {},
"cachedResponseWillBeUsed": {},
"cacheKeyWillBeUsed": {},
"cacheWillUpdate": {},
"fetchDidFail": {},
"fetchDidSucceed": {},
"handlerDidComplete": {},
"handlerDidError": {},
"handlerDidRespond": {},
"handlerWillRespond": {},
"handlerWillStart": {},
"requestWillFetch": {}
},
"additionalProperties": false
},
"CacheDidUpdateCallback": {
"type": "object",
"additionalProperties": false
},
"CachedResponseWillBeUsedCallback": {
"type": "object",
"additionalProperties": false
},
"CacheKeyWillBeUsedCallback": {
"type": "object",
"additionalProperties": false
},
"CacheWillUpdateCallback": {
"type": "object",
"additionalProperties": false
},
"FetchDidFailCallback": {
"type": "object",
"additionalProperties": false
},
"FetchDidSucceedCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerDidCompleteCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerDidErrorCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerDidRespondCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerWillRespondCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerWillStartCallback": {
"type": "object",
"additionalProperties": false
},
"RequestWillFetchCallback": {
"type": "object",
"additionalProperties": false
},
"RequestInit": {
"type": "object",
"properties": {
"body": {
"anyOf": [
{
"$ref": "#/definitions/ArrayBuffer"
},
{
"$ref": "#/definitions/ArrayBufferView"
},
{
"$ref": "#/definitions/ReadableStream"
},
{
"$ref": "#/definitions/Blob"
},
{
"$ref": "#/definitions/FormData"
},
{
"$ref": "#/definitions/URLSearchParams"
},
{
"type": [
"null",
"string"
]
}
]
},
"cache": {
"enum": [
"default",
"force-cache",
"no-cache",
"no-store",
"only-if-cached",
"reload"
],
"type": "string"
},
"credentials": {
"enum": [
"include",
"omit",
"same-origin"
],
"type": "string"
},
"headers": {
"anyOf": [
{
"$ref": "#/definitions/Record"
},
{
"type": "array",
"items": {
"type": "array",
"items": [
{
"type": "string"
},
{
"type": "string"
}
],
"minItems": 2,
"maxItems": 2
}
},
{
"$ref": "#/definitions/Headers"
}
]
},
"integrity": {
"type": "string"
},
"keepalive": {
"type": "boolean"
},
"method": {
"type": "string"
},
"mode": {
"enum": [
"cors",
"navigate",
"no-cors",
"same-origin"
],
"type": "string"
},
"redirect": {
"enum": [
"error",
"follow",
"manual"
],
"type": "string"
},
"referrer": {
"type": "string"
},
"referrerPolicy": {
"enum": [
"",
"no-referrer",
"no-referrer-when-downgrade",
"origin",
"origin-when-cross-origin",
"same-origin",
"strict-origin",
"strict-origin-when-cross-origin",
"unsafe-url"
],
"type": "string"
},
"signal": {
"anyOf": [
{
"$ref": "#/definitions/AbortSignal"
},
{
"type": "null"
}
]
},
"window": {
"type": "null"
}
},
"additionalProperties": false
},
"ArrayBuffer": {
"type": "object",
"properties": {
"byteLength": {
"type": "number"
},
"__@toStringTag@34": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"__@toStringTag@34",
"byteLength"
]
},
"ArrayBufferView": {
"type": "object",
"properties": {
"buffer": {
"$ref": "#/definitions/ArrayBufferLike"
},
"byteLength": {
"type": "number"
},
"byteOffset": {
"type": "number"
}
},
"additionalProperties": false,
"required": [
"buffer",
"byteLength",
"byteOffset"
]
},
"ArrayBufferLike": {
"anyOf": [
{
"$ref": "#/definitions/ArrayBuffer"
},
{
"$ref": "#/definitions/SharedArrayBuffer"
}
]
},
"SharedArrayBuffer": {
"type": "object",
"properties": {
"byteLength": {
"type": "number"
},
"__@species@494": {
"$ref": "#/definitions/SharedArrayBuffer"
},
"__@toStringTag@34": {
"type": "string",
"const": "SharedArrayBuffer"
}
},
"additionalProperties": false,
"required": [
"__@species@494",
"__@toStringTag@34",
"byteLength"
]
},
"ReadableStream": {
"type": "object",
"properties": {
"locked": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"locked"
]
},
"Blob": {
"type": "object",
"properties": {
"size": {
"type": "number"
},
"type": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"size",
"type"
]
},
"FormData": {
"type": "object",
"additionalProperties": false
},
"URLSearchParams": {
"type": "object",
"properties": {
"size": {
"description": "The total number of parameter entries.",
"type": "number"
}
},
"additionalProperties": false,
"required": [
"size"
]
},
"Record": {
"type": "object",
"additionalProperties": false
},
"Headers": {
"type": "object",
"additionalProperties": false
},
"AbortSignal": {},
"RouteMatchCallback": {}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
================================================
FILE: packages/workbox-build/src/schema/GetManifestOptions.json
================================================
{
"additionalProperties": false,
"type": "object",
"properties": {
"additionalManifestEntries": {
"description": "A list of entries to be precached, in addition to any entries that are\ngenerated as part of the build configuration.",
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/ManifestEntry"
},
{
"type": "string"
}
]
}
},
"dontCacheBustURLsMatching": {
"description": "Assets that match this will be assumed to be uniquely versioned via their\nURL, and exempted from the normal HTTP cache-busting that's done when\npopulating the precache. While not required, it's recommended that if your\nexisting build process already inserts a `[hash]` value into each filename,\nyou provide a RegExp that will detect that, as it will reduce the bandwidth\nconsumed when precaching.",
"$ref": "#/definitions/RegExp"
},
"manifestTransforms": {
"description": "One or more functions which will be applied sequentially against the\ngenerated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are\nalso specified, their corresponding transformations will be applied first.",
"type": "array",
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"description": "This value can be used to determine the maximum size of files that will be\nprecached. This prevents you from inadvertently precaching very large files\nthat might have accidentally matched one of your patterns.",
"default": 2097152,
"type": "number"
},
"modifyURLPrefix": {
"description": "An object mapping string prefixes to replacement string values. This can be\nused to, e.g., remove or add a path prefix from a manifest entry if your\nweb hosting setup doesn't match your local filesystem setup. As an\nalternative with more flexibility, you can use the `manifestTransforms`\noption and provide a function that modifies the entries in the manifest\nusing whatever logic you provide.\n\nExample usage:\n\n```\n// Replace a '/dist/' prefix with '/', and also prepend\n// '/static' to every URL.\nmodifyURLPrefix: {\n '/dist/': '/',\n '': '/static',\n}\n```",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"globFollow": {
"description": "Determines whether or not symlinks are followed when generating the\nprecache manifest. For more information, see the definition of `follow` in\nthe `glob` [documentation](https://github.com/isaacs/node-glob#options).",
"default": true,
"type": "boolean"
},
"globIgnores": {
"description": "A set of patterns matching files to always exclude when generating the\nprecache manifest. For more information, see the definition of `ignore` in\nthe `glob` [documentation](https://github.com/isaacs/node-glob#options).",
"default": [
"**/node_modules/**/*"
],
"type": "array",
"items": {
"type": "string"
}
},
"globPatterns": {
"description": "Files matching any of these patterns will be included in the precache\nmanifest. For more information, see the\n[`glob` primer](https://github.com/isaacs/node-glob#glob-primer).",
"default": [
"**/*.{js,wasm,css,html}"
],
"type": "array",
"items": {
"type": "string"
}
},
"templatedURLs": {
"description": "If a URL is rendered based on some server-side logic, its contents may\ndepend on multiple files or on some other unique string value. The keys in\nthis object are server-rendered URLs. If the values are an array of\nstrings, they will be interpreted as `glob` patterns, and the contents of\nany files matching the patterns will be used to uniquely version the URL.\nIf used with a single string, it will be interpreted as unique versioning\ninformation that you've generated for a given URL.",
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
}
},
"globDirectory": {
"description": "The local directory you wish to match `globPatterns` against. The path is\nrelative to the current directory.",
"type": "string"
}
},
"required": [
"globDirectory"
],
"definitions": {
"ManifestEntry": {
"type": "object",
"properties": {
"integrity": {
"type": "string"
},
"revision": {
"type": [
"null",
"string"
]
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"revision",
"url"
]
},
"RegExp": {
"type": "object",
"properties": {
"source": {
"type": "string"
},
"global": {
"type": "boolean"
},
"ignoreCase": {
"type": "boolean"
},
"multiline": {
"type": "boolean"
},
"lastIndex": {
"type": "number"
},
"flags": {
"type": "string"
},
"sticky": {
"type": "boolean"
},
"unicode": {
"type": "boolean"
},
"dotAll": {
"type": "boolean"
},
"hasIndices": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"dotAll",
"flags",
"global",
"hasIndices",
"ignoreCase",
"lastIndex",
"multiline",
"source",
"sticky",
"unicode"
]
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
================================================
FILE: packages/workbox-build/src/schema/InjectManifestOptions.json
================================================
{
"additionalProperties": false,
"type": "object",
"properties": {
"additionalManifestEntries": {
"description": "A list of entries to be precached, in addition to any entries that are\ngenerated as part of the build configuration.",
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/ManifestEntry"
},
{
"type": "string"
}
]
}
},
"dontCacheBustURLsMatching": {
"description": "Assets that match this will be assumed to be uniquely versioned via their\nURL, and exempted from the normal HTTP cache-busting that's done when\npopulating the precache. While not required, it's recommended that if your\nexisting build process already inserts a `[hash]` value into each filename,\nyou provide a RegExp that will detect that, as it will reduce the bandwidth\nconsumed when precaching.",
"$ref": "#/definitions/RegExp"
},
"manifestTransforms": {
"description": "One or more functions which will be applied sequentially against the\ngenerated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are\nalso specified, their corresponding transformations will be applied first.",
"type": "array",
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"description": "This value can be used to determine the maximum size of files that will be\nprecached. This prevents you from inadvertently precaching very large files\nthat might have accidentally matched one of your patterns.",
"default": 2097152,
"type": "number"
},
"modifyURLPrefix": {
"description": "An object mapping string prefixes to replacement string values. This can be\nused to, e.g., remove or add a path prefix from a manifest entry if your\nweb hosting setup doesn't match your local filesystem setup. As an\nalternative with more flexibility, you can use the `manifestTransforms`\noption and provide a function that modifies the entries in the manifest\nusing whatever logic you provide.\n\nExample usage:\n\n```\n// Replace a '/dist/' prefix with '/', and also prepend\n// '/static' to every URL.\nmodifyURLPrefix: {\n '/dist/': '/',\n '': '/static',\n}\n```",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"globFollow": {
"description": "Determines whether or not symlinks are followed when generating the\nprecache manifest. For more information, see the definition of `follow` in\nthe `glob` [documentation](https://github.com/isaacs/node-glob#options).",
"default": true,
"type": "boolean"
},
"globIgnores": {
"description": "A set of patterns matching files to always exclude when generating the\nprecache manifest. For more information, see the definition of `ignore` in\nthe `glob` [documentation](https://github.com/isaacs/node-glob#options).",
"default": [
"**/node_modules/**/*"
],
"type": "array",
"items": {
"type": "string"
}
},
"globPatterns": {
"description": "Files matching any of these patterns will be included in the precache\nmanifest. For more information, see the\n[`glob` primer](https://github.com/isaacs/node-glob#glob-primer).",
"default": [
"**/*.{js,wasm,css,html}"
],
"type": "array",
"items": {
"type": "string"
}
},
"templatedURLs": {
"description": "If a URL is rendered based on some server-side logic, its contents may\ndepend on multiple files or on some other unique string value. The keys in\nthis object are server-rendered URLs. If the values are an array of\nstrings, they will be interpreted as `glob` patterns, and the contents of\nany files matching the patterns will be used to uniquely version the URL.\nIf used with a single string, it will be interpreted as unique versioning\ninformation that you've generated for a given URL.",
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "string"
}
]
}
},
"injectionPoint": {
"description": "The string to find inside of the `swSrc` file. Once found, it will be\nreplaced by the generated precache manifest.",
"default": "self.__WB_MANIFEST",
"type": "string"
},
"swSrc": {
"description": "The path and filename of the service worker file that will be read during\nthe build process, relative to the current working directory.",
"type": "string"
},
"swDest": {
"description": "The path and filename of the service worker file that will be created by\nthe build process, relative to the current working directory. It must end\nin '.js'.",
"type": "string"
},
"globDirectory": {
"description": "The local directory you wish to match `globPatterns` against. The path is\nrelative to the current directory.",
"type": "string"
}
},
"required": [
"globDirectory",
"swDest",
"swSrc"
],
"definitions": {
"ManifestEntry": {
"type": "object",
"properties": {
"integrity": {
"type": "string"
},
"revision": {
"type": [
"null",
"string"
]
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"revision",
"url"
]
},
"RegExp": {
"type": "object",
"properties": {
"source": {
"type": "string"
},
"global": {
"type": "boolean"
},
"ignoreCase": {
"type": "boolean"
},
"multiline": {
"type": "boolean"
},
"lastIndex": {
"type": "number"
},
"flags": {
"type": "string"
},
"sticky": {
"type": "boolean"
},
"unicode": {
"type": "boolean"
},
"dotAll": {
"type": "boolean"
},
"hasIndices": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"dotAll",
"flags",
"global",
"hasIndices",
"ignoreCase",
"lastIndex",
"multiline",
"source",
"sticky",
"unicode"
]
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
================================================
FILE: packages/workbox-build/src/schema/WebpackGenerateSWOptions.json
================================================
{
"additionalProperties": false,
"type": "object",
"properties": {
"additionalManifestEntries": {
"description": "A list of entries to be precached, in addition to any entries that are\ngenerated as part of the build configuration.",
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/ManifestEntry"
},
{
"type": "string"
}
]
}
},
"dontCacheBustURLsMatching": {
"description": "Assets that match this will be assumed to be uniquely versioned via their\nURL, and exempted from the normal HTTP cache-busting that's done when\npopulating the precache. While not required, it's recommended that if your\nexisting build process already inserts a `[hash]` value into each filename,\nyou provide a RegExp that will detect that, as it will reduce the bandwidth\nconsumed when precaching.",
"$ref": "#/definitions/RegExp"
},
"manifestTransforms": {
"description": "One or more functions which will be applied sequentially against the\ngenerated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are\nalso specified, their corresponding transformations will be applied first.",
"type": "array",
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"description": "This value can be used to determine the maximum size of files that will be\nprecached. This prevents you from inadvertently precaching very large files\nthat might have accidentally matched one of your patterns.",
"default": 2097152,
"type": "number"
},
"modifyURLPrefix": {
"description": "An object mapping string prefixes to replacement string values. This can be\nused to, e.g., remove or add a path prefix from a manifest entry if your\nweb hosting setup doesn't match your local filesystem setup. As an\nalternative with more flexibility, you can use the `manifestTransforms`\noption and provide a function that modifies the entries in the manifest\nusing whatever logic you provide.\n\nExample usage:\n\n```\n// Replace a '/dist/' prefix with '/', and also prepend\n// '/static' to every URL.\nmodifyURLPrefix: {\n '/dist/': '/',\n '': '/static',\n}\n```",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"chunks": {
"description": "One or more chunk names whose corresponding output files should be included\nin the precache manifest.",
"type": "array",
"items": {
"type": "string"
}
},
"exclude": {
"description": "One or more specifiers used to exclude assets from the precache manifest.\nThis is interpreted following\n[the same rules](https://webpack.js.org/configuration/module/#condition)\nas `webpack`'s standard `exclude` option.\nIf not provided, the default value is `[/\\.map$/, /^manifest.*\\.js$]`.",
"type": "array",
"items": {}
},
"excludeChunks": {
"description": "One or more chunk names whose corresponding output files should be excluded\nfrom the precache manifest.",
"type": "array",
"items": {
"type": "string"
}
},
"include": {
"description": "One or more specifiers used to include assets in the precache manifest.\nThis is interpreted following\n[the same rules](https://webpack.js.org/configuration/module/#condition)\nas `webpack`'s standard `include` option.",
"type": "array",
"items": {}
},
"mode": {
"description": "If set to 'production', then an optimized service worker bundle that\nexcludes debugging info will be produced. If not explicitly configured\nhere, the `process.env.NODE_ENV` value will be used, and failing that, it\nwill fall back to `'production'`.",
"default": "production",
"type": [
"null",
"string"
]
},
"babelPresetEnvTargets": {
"description": "The [targets](https://babeljs.io/docs/en/babel-preset-env#targets) to pass\nto `babel-preset-env` when transpiling the service worker bundle.",
"default": [
"chrome >= 56"
],
"type": "array",
"items": {
"type": "string"
}
},
"cacheId": {
"description": "An optional ID to be prepended to cache names. This is primarily useful for\nlocal development where multiple sites may be served from the same\n`http://localhost:port` origin.",
"type": [
"null",
"string"
]
},
"cleanupOutdatedCaches": {
"description": "Whether or not Workbox should attempt to identify and delete any precaches\ncreated by older, incompatible versions.",
"default": false,
"type": "boolean"
},
"clientsClaim": {
"description": "Whether or not the service worker should [start controlling](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim)\nany existing clients as soon as it activates.",
"default": false,
"type": "boolean"
},
"directoryIndex": {
"description": "If a navigation request for a URL ending in `/` fails to match a precached\nURL, this value will be appended to the URL and that will be checked for a\nprecache match. This should be set to what your web server is using for its\ndirectory index.",
"type": [
"null",
"string"
]
},
"disableDevLogs": {
"default": false,
"type": "boolean"
},
"ignoreURLParametersMatching": {
"description": "Any search parameter names that match against one of the RegExp in this\narray will be removed before looking for a precache match. This is useful\nif your users might request URLs that contain, for example, URL parameters\nused to track the source of the traffic. If not provided, the default value\nis `[/^utm_/, /^fbclid$/]`.",
"type": "array",
"items": {
"$ref": "#/definitions/RegExp"
}
},
"importScripts": {
"description": "A list of JavaScript files that should be passed to\n[`importScripts()`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts)\ninside the generated service worker file. This is useful when you want to\nlet Workbox create your top-level service worker file, but want to include\nsome additional code, such as a push event listener.",
"type": "array",
"items": {
"type": "string"
}
},
"inlineWorkboxRuntime": {
"description": "Whether the runtime code for the Workbox library should be included in the\ntop-level service worker, or split into a separate file that needs to be\ndeployed alongside the service worker. Keeping the runtime separate means\nthat users will not have to re-download the Workbox code each time your\ntop-level service worker changes.",
"default": false,
"type": "boolean"
},
"navigateFallback": {
"description": "If specified, all\n[navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests)\nfor URLs that aren't precached will be fulfilled with the HTML at the URL\nprovided. You must pass in the URL of an HTML document that is listed in\nyour precache manifest. This is meant to be used in a Single Page App\nscenario, in which you want all navigations to use common\n[App Shell HTML](https://developers.google.com/web/fundamentals/architecture/app-shell).",
"default": null,
"type": [
"null",
"string"
]
},
"navigateFallbackAllowlist": {
"description": "An optional array of regular expressions that restricts which URLs the\nconfigured `navigateFallback` behavior applies to. This is useful if only a\nsubset of your site's URLs should be treated as being part of a\n[Single Page App](https://en.wikipedia.org/wiki/Single-page_application).\nIf both `navigateFallbackDenylist` and `navigateFallbackAllowlist` are\nconfigured, the denylist takes precedent.\n\n*Note*: These RegExps may be evaluated against every destination URL during\na navigation. Avoid using\n[complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),\nor else your users may see delays when navigating your site.",
"type": "array",
"items": {
"$ref": "#/definitions/RegExp"
}
},
"navigateFallbackDenylist": {
"description": "An optional array of regular expressions that restricts which URLs the\nconfigured `navigateFallback` behavior applies to. This is useful if only a\nsubset of your site's URLs should be treated as being part of a\n[Single Page App](https://en.wikipedia.org/wiki/Single-page_application).\nIf both `navigateFallbackDenylist` and `navigateFallbackAllowlist` are\nconfigured, the denylist takes precedence.\n\n*Note*: These RegExps may be evaluated against every destination URL during\na navigation. Avoid using\n[complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),\nor else your users may see delays when navigating your site.",
"type": "array",
"items": {
"$ref": "#/definitions/RegExp"
}
},
"navigationPreload": {
"description": "Whether or not to enable\n[navigation preload](https://developers.google.com/web/tools/workbox/modules/workbox-navigation-preload)\nin the generated service worker. When set to true, you must also use\n`runtimeCaching` to set up an appropriate response strategy that will match\nnavigation requests, and make use of the preloaded response.",
"default": false,
"type": "boolean"
},
"offlineGoogleAnalytics": {
"description": "Controls whether or not to include support for\n[offline Google Analytics](https://developers.google.com/web/tools/workbox/guides/enable-offline-analytics).\nWhen `true`, the call to `workbox-google-analytics`'s `initialize()` will\nbe added to your generated service worker. When set to an `Object`, that\nobject will be passed in to the `initialize()` call, allowing you to\ncustomize the behavior.",
"default": false,
"anyOf": [
{
"$ref": "#/definitions/GoogleAnalyticsInitializeOptions"
},
{
"type": "boolean"
}
]
},
"runtimeCaching": {
"description": "When using Workbox's build tools to generate your service worker, you can\nspecify one or more runtime caching configurations. These are then\ntranslated to {@link workbox-routing.registerRoute} calls using the match\nand handler configuration you define.\n\nFor all of the options, see the {@link workbox-build.RuntimeCaching}\ndocumentation. The example below shows a typical configuration, with two\nruntime routes defined:",
"type": "array",
"items": {
"$ref": "#/definitions/RuntimeCaching"
}
},
"skipWaiting": {
"description": "Whether to add an unconditional call to [`skipWaiting()`](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#skip_the_waiting_phase)\nto the generated service worker. If `false`, then a `message` listener will\nbe added instead, allowing client pages to trigger `skipWaiting()` by\ncalling `postMessage({type: 'SKIP_WAITING'})` on a waiting service worker.",
"default": false,
"type": "boolean"
},
"sourcemap": {
"description": "Whether to create a sourcemap for the generated service worker files.",
"default": true,
"type": "boolean"
},
"importScriptsViaChunks": {
"description": "One or more names of webpack chunks. The content of those chunks will be\nincluded in the generated service worker, via a call to `importScripts()`.",
"type": "array",
"items": {
"type": "string"
}
},
"swDest": {
"description": "The asset name of the service worker file created by this plugin.",
"default": "service-worker.js",
"type": "string"
}
},
"definitions": {
"ManifestEntry": {
"type": "object",
"properties": {
"integrity": {
"type": "string"
},
"revision": {
"type": [
"null",
"string"
]
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"revision",
"url"
]
},
"RegExp": {
"type": "object",
"properties": {
"source": {
"type": "string"
},
"global": {
"type": "boolean"
},
"ignoreCase": {
"type": "boolean"
},
"multiline": {
"type": "boolean"
},
"lastIndex": {
"type": "number"
},
"flags": {
"type": "string"
},
"sticky": {
"type": "boolean"
},
"unicode": {
"type": "boolean"
},
"dotAll": {
"type": "boolean"
},
"hasIndices": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"dotAll",
"flags",
"global",
"hasIndices",
"ignoreCase",
"lastIndex",
"multiline",
"source",
"sticky",
"unicode"
]
},
"GoogleAnalyticsInitializeOptions": {
"type": "object",
"properties": {
"cacheName": {
"type": "string"
},
"parameterOverrides": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"hitFilter": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
"RuntimeCaching": {
"type": "object",
"properties": {
"handler": {
"description": "This determines how the runtime route will generate a response.\nTo use one of the built-in {@link workbox-strategies}, provide its name,\nlike `'NetworkFirst'`.\nAlternatively, this can be a {@link workbox-core.RouteHandler} callback\nfunction with custom response logic.",
"anyOf": [
{
"$ref": "#/definitions/RouteHandlerCallback"
},
{
"$ref": "#/definitions/RouteHandlerObject"
},
{
"enum": [
"CacheFirst",
"CacheOnly",
"NetworkFirst",
"NetworkOnly",
"StaleWhileRevalidate"
],
"type": "string"
}
]
},
"method": {
"description": "The HTTP method to match against. The default value of `'GET'` is normally\nsufficient, unless you explicitly need to match `'POST'`, `'PUT'`, or\nanother type of request.",
"default": "GET",
"enum": [
"DELETE",
"GET",
"HEAD",
"PATCH",
"POST",
"PUT"
],
"type": "string"
},
"options": {
"type": "object",
"properties": {
"backgroundSync": {
"description": "Configuring this will add a\n{@link workbox-background-sync.BackgroundSyncPlugin} instance to the\n{@link workbox-strategies} configured in `handler`.",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"options": {
"$ref": "#/definitions/QueueOptions"
}
},
"additionalProperties": false,
"required": [
"name"
]
},
"broadcastUpdate": {
"description": "Configuring this will add a\n{@link workbox-broadcast-update.BroadcastUpdatePlugin} instance to the\n{@link workbox-strategies} configured in `handler`.",
"type": "object",
"properties": {
"channelName": {
"type": "string"
},
"options": {
"$ref": "#/definitions/BroadcastCacheUpdateOptions"
}
},
"additionalProperties": false,
"required": [
"options"
]
},
"cacheableResponse": {
"description": "Configuring this will add a\n{@link workbox-cacheable-response.CacheableResponsePlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/CacheableResponseOptions"
},
"cacheName": {
"description": "If provided, this will set the `cacheName` property of the\n{@link workbox-strategies} configured in `handler`.",
"type": [
"null",
"string"
]
},
"expiration": {
"description": "Configuring this will add a\n{@link workbox-expiration.ExpirationPlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/ExpirationPluginOptions"
},
"networkTimeoutSeconds": {
"description": "If provided, this will set the `networkTimeoutSeconds` property of the\n{@link workbox-strategies} configured in `handler`. Note that only\n`'NetworkFirst'` and `'NetworkOnly'` support `networkTimeoutSeconds`.",
"type": "number"
},
"plugins": {
"description": "Configuring this allows the use of one or more Workbox plugins that\ndon't have \"shortcut\" options (like `expiration` for\n{@link workbox-expiration.ExpirationPlugin}). The plugins provided here\nwill be added to the {@link workbox-strategies} configured in `handler`.",
"type": "array",
"items": {
"$ref": "#/definitions/WorkboxPlugin"
}
},
"precacheFallback": {
"description": "Configuring this will add a\n{@link workbox-precaching.PrecacheFallbackPlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"type": "object",
"properties": {
"fallbackURL": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"fallbackURL"
]
},
"rangeRequests": {
"description": "Enabling this will add a\n{@link workbox-range-requests.RangeRequestsPlugin} instance to\nthe {@link workbox-strategies} configured in `handler`.",
"type": "boolean"
},
"fetchOptions": {
"description": "Configuring this will pass along the `fetchOptions` value to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/RequestInit"
},
"matchOptions": {
"description": "Configuring this will pass along the `matchOptions` value to\nthe {@link workbox-strategies} configured in `handler`.",
"$ref": "#/definitions/CacheQueryOptions"
}
},
"additionalProperties": false
},
"urlPattern": {
"description": "This match criteria determines whether the configured handler will\ngenerate a response for any requests that don't match one of the precached\nURLs. If multiple `RuntimeCaching` routes are defined, then the first one\nwhose `urlPattern` matches will be the one that responds.\n\nThis value directly maps to the first parameter passed to\n{@link workbox-routing.registerRoute}. It's recommended to use a\n{@link workbox-core.RouteMatchCallback} function for greatest flexibility.",
"anyOf": [
{
"$ref": "#/definitions/RegExp"
},
{
"$ref": "#/definitions/RouteMatchCallback"
},
{
"type": "string"
}
]
}
},
"additionalProperties": false,
"required": [
"handler",
"urlPattern"
]
},
"RouteHandlerCallback": {},
"RouteHandlerObject": {
"description": "An object with a `handle` method of type `RouteHandlerCallback`.\n\nA `Route` object can be created with either an `RouteHandlerCallback`\nfunction or this `RouteHandler` object. The benefit of the `RouteHandler`\nis it can be extended (as is done by the `workbox-strategies` package).",
"type": "object",
"properties": {
"handle": {
"$ref": "#/definitions/RouteHandlerCallback"
}
},
"additionalProperties": false,
"required": [
"handle"
]
},
"QueueOptions": {
"type": "object",
"properties": {
"forceSyncFallback": {
"type": "boolean"
},
"maxRetentionTime": {
"type": "number"
},
"onSync": {
"$ref": "#/definitions/OnSyncCallback"
}
},
"additionalProperties": false
},
"OnSyncCallback": {},
"BroadcastCacheUpdateOptions": {
"type": "object",
"properties": {
"headersToCheck": {
"type": "array",
"items": {
"type": "string"
}
},
"generatePayload": {
"type": "object",
"additionalProperties": false
},
"notifyAllClients": {
"type": "boolean"
}
},
"additionalProperties": false
},
"CacheableResponseOptions": {
"type": "object",
"properties": {
"statuses": {
"type": "array",
"items": {
"type": "number"
}
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"additionalProperties": false
},
"ExpirationPluginOptions": {
"type": "object",
"properties": {
"maxEntries": {
"type": "number"
},
"maxAgeSeconds": {
"type": "number"
},
"matchOptions": {
"$ref": "#/definitions/CacheQueryOptions"
},
"purgeOnQuotaError": {
"type": "boolean"
}
},
"additionalProperties": false
},
"CacheQueryOptions": {
"type": "object",
"properties": {
"ignoreMethod": {
"type": "boolean"
},
"ignoreSearch": {
"type": "boolean"
},
"ignoreVary": {
"type": "boolean"
}
},
"additionalProperties": false
},
"WorkboxPlugin": {
"description": "An object with optional lifecycle callback properties for the fetch and\ncache operations.",
"type": "object",
"properties": {
"cacheDidUpdate": {},
"cachedResponseWillBeUsed": {},
"cacheKeyWillBeUsed": {},
"cacheWillUpdate": {},
"fetchDidFail": {},
"fetchDidSucceed": {},
"handlerDidComplete": {},
"handlerDidError": {},
"handlerDidRespond": {},
"handlerWillRespond": {},
"handlerWillStart": {},
"requestWillFetch": {}
},
"additionalProperties": false
},
"CacheDidUpdateCallback": {
"type": "object",
"additionalProperties": false
},
"CachedResponseWillBeUsedCallback": {
"type": "object",
"additionalProperties": false
},
"CacheKeyWillBeUsedCallback": {
"type": "object",
"additionalProperties": false
},
"CacheWillUpdateCallback": {
"type": "object",
"additionalProperties": false
},
"FetchDidFailCallback": {
"type": "object",
"additionalProperties": false
},
"FetchDidSucceedCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerDidCompleteCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerDidErrorCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerDidRespondCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerWillRespondCallback": {
"type": "object",
"additionalProperties": false
},
"HandlerWillStartCallback": {
"type": "object",
"additionalProperties": false
},
"RequestWillFetchCallback": {
"type": "object",
"additionalProperties": false
},
"RequestInit": {
"type": "object",
"properties": {
"body": {
"anyOf": [
{
"$ref": "#/definitions/ArrayBuffer"
},
{
"$ref": "#/definitions/ArrayBufferView"
},
{
"$ref": "#/definitions/ReadableStream"
},
{
"$ref": "#/definitions/Blob"
},
{
"$ref": "#/definitions/FormData"
},
{
"$ref": "#/definitions/URLSearchParams"
},
{
"type": [
"null",
"string"
]
}
]
},
"cache": {
"enum": [
"default",
"force-cache",
"no-cache",
"no-store",
"only-if-cached",
"reload"
],
"type": "string"
},
"credentials": {
"enum": [
"include",
"omit",
"same-origin"
],
"type": "string"
},
"headers": {
"anyOf": [
{
"$ref": "#/definitions/Record"
},
{
"type": "array",
"items": {
"type": "array",
"items": [
{
"type": "string"
},
{
"type": "string"
}
],
"minItems": 2,
"maxItems": 2
}
},
{
"$ref": "#/definitions/Headers"
}
]
},
"integrity": {
"type": "string"
},
"keepalive": {
"type": "boolean"
},
"method": {
"type": "string"
},
"mode": {
"enum": [
"cors",
"navigate",
"no-cors",
"same-origin"
],
"type": "string"
},
"redirect": {
"enum": [
"error",
"follow",
"manual"
],
"type": "string"
},
"referrer": {
"type": "string"
},
"referrerPolicy": {
"enum": [
"",
"no-referrer",
"no-referrer-when-downgrade",
"origin",
"origin-when-cross-origin",
"same-origin",
"strict-origin",
"strict-origin-when-cross-origin",
"unsafe-url"
],
"type": "string"
},
"signal": {
"anyOf": [
{
"$ref": "#/definitions/AbortSignal"
},
{
"type": "null"
}
]
},
"window": {
"type": "null"
}
},
"additionalProperties": false
},
"ArrayBuffer": {
"type": "object",
"properties": {
"byteLength": {
"type": "number"
},
"__@toStringTag@34": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"__@toStringTag@34",
"byteLength"
]
},
"ArrayBufferView": {
"type": "object",
"properties": {
"buffer": {
"$ref": "#/definitions/ArrayBufferLike"
},
"byteLength": {
"type": "number"
},
"byteOffset": {
"type": "number"
}
},
"additionalProperties": false,
"required": [
"buffer",
"byteLength",
"byteOffset"
]
},
"ArrayBufferLike": {
"anyOf": [
{
"$ref": "#/definitions/ArrayBuffer"
},
{
"$ref": "#/definitions/SharedArrayBuffer"
}
]
},
"SharedArrayBuffer": {
"type": "object",
"properties": {
"byteLength": {
"type": "number"
},
"__@species@494": {
"$ref": "#/definitions/SharedArrayBuffer"
},
"__@toStringTag@34": {
"type": "string",
"const": "SharedArrayBuffer"
}
},
"additionalProperties": false,
"required": [
"__@species@494",
"__@toStringTag@34",
"byteLength"
]
},
"ReadableStream": {
"type": "object",
"properties": {
"locked": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"locked"
]
},
"Blob": {
"type": "object",
"properties": {
"size": {
"type": "number"
},
"type": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"size",
"type"
]
},
"FormData": {
"type": "object",
"additionalProperties": false
},
"URLSearchParams": {
"type": "object",
"properties": {
"size": {
"description": "The total number of parameter entries.",
"type": "number"
}
},
"additionalProperties": false,
"required": [
"size"
]
},
"Record": {
"type": "object",
"additionalProperties": false
},
"Headers": {
"type": "object",
"additionalProperties": false
},
"AbortSignal": {},
"RouteMatchCallback": {}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
================================================
FILE: packages/workbox-build/src/schema/WebpackInjectManifestOptions.json
================================================
{
"additionalProperties": false,
"type": "object",
"properties": {
"additionalManifestEntries": {
"description": "A list of entries to be precached, in addition to any entries that are\ngenerated as part of the build configuration.",
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/ManifestEntry"
},
{
"type": "string"
}
]
}
},
"dontCacheBustURLsMatching": {
"description": "Assets that match this will be assumed to be uniquely versioned via their\nURL, and exempted from the normal HTTP cache-busting that's done when\npopulating the precache. While not required, it's recommended that if your\nexisting build process already inserts a `[hash]` value into each filename,\nyou provide a RegExp that will detect that, as it will reduce the bandwidth\nconsumed when precaching.",
"$ref": "#/definitions/RegExp"
},
"manifestTransforms": {
"description": "One or more functions which will be applied sequentially against the\ngenerated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are\nalso specified, their corresponding transformations will be applied first.",
"type": "array",
"items": {}
},
"maximumFileSizeToCacheInBytes": {
"description": "This value can be used to determine the maximum size of files that will be\nprecached. This prevents you from inadvertently precaching very large files\nthat might have accidentally matched one of your patterns.",
"default": 2097152,
"type": "number"
},
"modifyURLPrefix": {
"description": "An object mapping string prefixes to replacement string values. This can be\nused to, e.g., remove or add a path prefix from a manifest entry if your\nweb hosting setup doesn't match your local filesystem setup. As an\nalternative with more flexibility, you can use the `manifestTransforms`\noption and provide a function that modifies the entries in the manifest\nusing whatever logic you provide.\n\nExample usage:\n\n```\n// Replace a '/dist/' prefix with '/', and also prepend\n// '/static' to every URL.\nmodifyURLPrefix: {\n '/dist/': '/',\n '': '/static',\n}\n```",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"chunks": {
"description": "One or more chunk names whose corresponding output files should be included\nin the precache manifest.",
"type": "array",
"items": {
"type": "string"
}
},
"exclude": {
"description": "One or more specifiers used to exclude assets from the precache manifest.\nThis is interpreted following\n[the same rules](https://webpack.js.org/configuration/module/#condition)\nas `webpack`'s standard `exclude` option.\nIf not provided, the default value is `[/\\.map$/, /^manifest.*\\.js$]`.",
"type": "array",
"items": {}
},
"excludeChunks": {
"description": "One or more chunk names whose corresponding output files should be excluded\nfrom the precache manifest.",
"type": "array",
"items": {
"type": "string"
}
},
"include": {
"description": "One or more specifiers used to include assets in the precache manifest.\nThis is interpreted following\n[the same rules](https://webpack.js.org/configuration/module/#condition)\nas `webpack`'s standard `include` option.",
"type": "array",
"items": {}
},
"mode": {
"description": "If set to 'production', then an optimized service worker bundle that\nexcludes debugging info will be produced. If not explicitly configured\nhere, the `mode` value configured in the current `webpack` compilation\nwill be used.",
"type": [
"null",
"string"
]
},
"injectionPoint": {
"description": "The string to find inside of the `swSrc` file. Once found, it will be\nreplaced by the generated precache manifest.",
"default": "self.__WB_MANIFEST",
"type": "string"
},
"swSrc": {
"description": "The path and filename of the service worker file that will be read during\nthe build process, relative to the current working directory.",
"type": "string"
},
"compileSrc": {
"description": "When `true` (the default), the `swSrc` file will be compiled by webpack.\nWhen `false`, compilation will not occur (and `webpackCompilationPlugins`\ncan't be used.) Set to `false` if you want to inject the manifest into,\ne.g., a JSON file.",
"default": true,
"type": "boolean"
},
"swDest": {
"description": "The asset name of the service worker file that will be created by this\nplugin. If omitted, the name will be based on the `swSrc` name.",
"type": "string"
},
"webpackCompilationPlugins": {
"description": "Optional `webpack` plugins that will be used when compiling the `swSrc`\ninput file. Only valid if `compileSrc` is `true`.",
"type": "array",
"items": {}
}
},
"required": [
"swSrc"
],
"definitions": {
"ManifestEntry": {
"type": "object",
"properties": {
"integrity": {
"type": "string"
},
"revision": {
"type": [
"null",
"string"
]
},
"url": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"revision",
"url"
]
},
"RegExp": {
"type": "object",
"properties": {
"source": {
"type": "string"
},
"global": {
"type": "boolean"
},
"ignoreCase": {
"type": "boolean"
},
"multiline": {
"type": "boolean"
},
"lastIndex": {
"type": "number"
},
"flags": {
"type": "string"
},
"sticky": {
"type": "boolean"
},
"unicode": {
"type": "boolean"
},
"dotAll": {
"type": "boolean"
},
"hasIndices": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"dotAll",
"flags",
"global",
"hasIndices",
"ignoreCase",
"lastIndex",
"multiline",
"source",
"sticky",
"unicode"
]
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
================================================
FILE: packages/workbox-build/src/strip-comments.d.ts
================================================
declare module 'strip-comments';
================================================
FILE: packages/workbox-build/src/templates/sw-template.ts
================================================
/*
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.
*/
export const swTemplate = `/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app.
* See https://goo.gl/nhQhGp
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
* See https://goo.gl/2aRDsh
*/
<% if (importScripts) { %>
importScripts(
<%= importScripts.map(JSON.stringify).join(',\\n ') %>
);
<% } %>
<% if (navigationPreload) { %><%= use('workbox-navigation-preload', 'enable') %>();<% } %>
<% if (cacheId) { %><%= use('workbox-core', 'setCacheNameDetails') %>({prefix: <%= JSON.stringify(cacheId) %>});<% } %>
<% if (skipWaiting) { %>
self.skipWaiting();
<% } else { %>
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
<% } %>
<% if (clientsClaim) { %><%= use('workbox-core', 'clientsClaim') %>();<% } %>
<% if (Array.isArray(manifestEntries) && manifestEntries.length > 0) {%>
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
<%= use('workbox-precaching', 'precacheAndRoute') %>(<%= JSON.stringify(manifestEntries, null, 2) %>, <%= precacheOptionsString %>);
<% if (cleanupOutdatedCaches) { %><%= use('workbox-precaching', 'cleanupOutdatedCaches') %>();<% } %>
<% if (navigateFallback) { %><%= use('workbox-routing', 'registerRoute') %>(new <%= use('workbox-routing', 'NavigationRoute') %>(<%= use('workbox-precaching', 'createHandlerBoundToURL') %>(<%= JSON.stringify(navigateFallback) %>)<% if (navigateFallbackAllowlist || navigateFallbackDenylist) { %>, {
<% if (navigateFallbackAllowlist) { %>allowlist: [<%= navigateFallbackAllowlist %>],<% } %>
<% if (navigateFallbackDenylist) { %>denylist: [<%= navigateFallbackDenylist %>],<% } %>
}<% } %>));<% } %>
<% } %>
<% if (runtimeCaching) { runtimeCaching.forEach(runtimeCachingString => {%><%= runtimeCachingString %><% });} %>
<% if (offlineAnalyticsConfigString) { %><%= use('workbox-google-analytics', 'initialize') %>(<%= offlineAnalyticsConfigString %>);<% } %>
<% if (disableDevLogs) { %>self.__WB_DISABLE_DEV_LOGS = true;<% } %>`;
================================================
FILE: packages/workbox-build/src/types.ts
================================================
import {PackageJson} from 'type-fest';
import {BroadcastCacheUpdateOptions} from 'workbox-broadcast-update/BroadcastCacheUpdate';
import {GoogleAnalyticsInitializeOptions} from 'workbox-google-analytics/initialize';
import {HTTPMethod} from 'workbox-routing/utils/constants';
import {QueueOptions} from 'workbox-background-sync/Queue';
import {RouteHandler, RouteMatchCallback} from 'workbox-core/types';
import {CacheableResponseOptions} from 'workbox-cacheable-response/CacheableResponse';
import {ExpirationPluginOptions} from 'workbox-expiration/ExpirationPlugin';
import {WorkboxPlugin} from 'workbox-core/types';
export interface ManifestEntry {
integrity?: string;
revision: string | null;
url: string;
}
export type StrategyName =
| 'CacheFirst'
| 'CacheOnly'
| 'NetworkFirst'
| 'NetworkOnly'
| 'StaleWhileRevalidate';
export interface RuntimeCaching {
/**
* This determines how the runtime route will generate a response.
* To use one of the built-in {@link workbox-strategies}, provide its name,
* like `'NetworkFirst'`.
* Alternatively, this can be a {@link workbox-core.RouteHandler} callback
* function with custom response logic.
*/
handler: RouteHandler | StrategyName;
/**
* The HTTP method to match against. The default value of `'GET'` is normally
* sufficient, unless you explicitly need to match `'POST'`, `'PUT'`, or
* another type of request.
* @default "GET"
*/
method?: HTTPMethod;
options?: {
/**
* Configuring this will add a
* {@link workbox-background-sync.BackgroundSyncPlugin} instance to the
* {@link workbox-strategies} configured in `handler`.
*/
backgroundSync?: {
name: string;
options?: QueueOptions;
};
/**
* Configuring this will add a
* {@link workbox-broadcast-update.BroadcastUpdatePlugin} instance to the
* {@link workbox-strategies} configured in `handler`.
*/
broadcastUpdate?: {
// TODO: This option is ignored since we switched to using postMessage().
// Remove it in the next major release.
channelName?: string;
options: BroadcastCacheUpdateOptions;
};
/**
* Configuring this will add a
* {@link workbox-cacheable-response.CacheableResponsePlugin} instance to
* the {@link workbox-strategies} configured in `handler`.
*/
cacheableResponse?: CacheableResponseOptions;
/**
* If provided, this will set the `cacheName` property of the
* {@link workbox-strategies} configured in `handler`.
*/
cacheName?: string | null;
/**
* Configuring this will add a
* {@link workbox-expiration.ExpirationPlugin} instance to
* the {@link workbox-strategies} configured in `handler`.
*/
expiration?: ExpirationPluginOptions;
/**
* If provided, this will set the `networkTimeoutSeconds` property of the
* {@link workbox-strategies} configured in `handler`. Note that only
* `'NetworkFirst'` and `'NetworkOnly'` support `networkTimeoutSeconds`.
*/
networkTimeoutSeconds?: number;
/**
* Configuring this allows the use of one or more Workbox plugins that
* don't have "shortcut" options (like `expiration` for
* {@link workbox-expiration.ExpirationPlugin}). The plugins provided here
* will be added to the {@link workbox-strategies} configured in `handler`.
*/
plugins?: Array;
/**
* Configuring this will add a
* {@link workbox-precaching.PrecacheFallbackPlugin} instance to
* the {@link workbox-strategies} configured in `handler`.
*/
precacheFallback?: {
fallbackURL: string;
};
/**
* Enabling this will add a
* {@link workbox-range-requests.RangeRequestsPlugin} instance to
* the {@link workbox-strategies} configured in `handler`.
*/
rangeRequests?: boolean;
/**
* Configuring this will pass along the `fetchOptions` value to
* the {@link workbox-strategies} configured in `handler`.
*/
fetchOptions?: RequestInit;
/**
* Configuring this will pass along the `matchOptions` value to
* the {@link workbox-strategies} configured in `handler`.
*/
matchOptions?: CacheQueryOptions;
};
/**
* This match criteria determines whether the configured handler will
* generate a response for any requests that don't match one of the precached
* URLs. If multiple `RuntimeCaching` routes are defined, then the first one
* whose `urlPattern` matches will be the one that responds.
*
* This value directly maps to the first parameter passed to
* {@link workbox-routing.registerRoute}. It's recommended to use a
* {@link workbox-core.RouteMatchCallback} function for greatest flexibility.
*/
urlPattern: RegExp | string | RouteMatchCallback;
}
export interface ManifestTransformResult {
manifest: Array;
warnings?: Array;
}
export type ManifestTransform = (
manifestEntries: Array,
compilation?: unknown,
) => Promise | ManifestTransformResult;
export interface BasePartial {
/**
* A list of entries to be precached, in addition to any entries that are
* generated as part of the build configuration.
*/
additionalManifestEntries?: Array;
/**
* Assets that match this will be assumed to be uniquely versioned via their
* URL, and exempted from the normal HTTP cache-busting that's done when
* populating the precache. While not required, it's recommended that if your
* existing build process already inserts a `[hash]` value into each filename,
* you provide a RegExp that will detect that, as it will reduce the bandwidth
* consumed when precaching.
*/
dontCacheBustURLsMatching?: RegExp;
/**
* One or more functions which will be applied sequentially against the
* generated manifest. If `modifyURLPrefix` or `dontCacheBustURLsMatching` are
* also specified, their corresponding transformations will be applied first.
*/
manifestTransforms?: Array;
/**
* This value can be used to determine the maximum size of files that will be
* precached. This prevents you from inadvertently precaching very large files
* that might have accidentally matched one of your patterns.
* @default 2097152
*/
maximumFileSizeToCacheInBytes?: number;
/**
* An object mapping string prefixes to replacement string values. This can be
* used to, e.g., remove or add a path prefix from a manifest entry if your
* web hosting setup doesn't match your local filesystem setup. As an
* alternative with more flexibility, you can use the `manifestTransforms`
* option and provide a function that modifies the entries in the manifest
* using whatever logic you provide.
*
* Example usage:
*
* ```
* // Replace a '/dist/' prefix with '/', and also prepend
* // '/static' to every URL.
* modifyURLPrefix: {
* '/dist/': '/',
* '': '/static',
* }
* ```
*/
modifyURLPrefix?: {
[key: string]: string;
};
}
export interface GeneratePartial {
/**
* The [targets](https://babeljs.io/docs/en/babel-preset-env#targets) to pass
* to `babel-preset-env` when transpiling the service worker bundle.
* @default ["chrome >= 56"]
*/
babelPresetEnvTargets?: Array;
/**
* An optional ID to be prepended to cache names. This is primarily useful for
* local development where multiple sites may be served from the same
* `http://localhost:port` origin.
*/
cacheId?: string | null;
/**
* Whether or not Workbox should attempt to identify and delete any precaches
* created by older, incompatible versions.
* @default false
*/
cleanupOutdatedCaches?: boolean;
/**
* Whether or not the service worker should [start controlling](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim)
* any existing clients as soon as it activates.
* @default false
*/
clientsClaim?: boolean;
/**
* If a navigation request for a URL ending in `/` fails to match a precached
* URL, this value will be appended to the URL and that will be checked for a
* precache match. This should be set to what your web server is using for its
* directory index.
*/
directoryIndex?: string | null;
/**
* @default false
*/
disableDevLogs?: boolean;
// We can't use the @default annotation here to assign the value via AJV, as
// an Array can't be serialized into JSON.
/**
* Any search parameter names that match against one of the RegExp in this
* array will be removed before looking for a precache match. This is useful
* if your users might request URLs that contain, for example, URL parameters
* used to track the source of the traffic. If not provided, the default value
* is `[/^utm_/, /^fbclid$/]`.
*
*/
ignoreURLParametersMatching?: Array;
/**
* A list of JavaScript files that should be passed to
* [`importScripts()`](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts)
* inside the generated service worker file. This is useful when you want to
* let Workbox create your top-level service worker file, but want to include
* some additional code, such as a push event listener.
*/
importScripts?: Array;
/**
* Whether the runtime code for the Workbox library should be included in the
* top-level service worker, or split into a separate file that needs to be
* deployed alongside the service worker. Keeping the runtime separate means
* that users will not have to re-download the Workbox code each time your
* top-level service worker changes.
* @default false
*/
inlineWorkboxRuntime?: boolean;
/**
* If set to 'production', then an optimized service worker bundle that
* excludes debugging info will be produced. If not explicitly configured
* here, the `process.env.NODE_ENV` value will be used, and failing that, it
* will fall back to `'production'`.
* @default "production"
*/
mode?: string | null;
/**
* If specified, all
* [navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests)
* for URLs that aren't precached will be fulfilled with the HTML at the URL
* provided. You must pass in the URL of an HTML document that is listed in
* your precache manifest. This is meant to be used in a Single Page App
* scenario, in which you want all navigations to use common
* [App Shell HTML](https://developers.google.com/web/fundamentals/architecture/app-shell).
* @default null
*/
navigateFallback?: string | null;
/**
* An optional array of regular expressions that restricts which URLs the
* configured `navigateFallback` behavior applies to. This is useful if only a
* subset of your site's URLs should be treated as being part of a
* [Single Page App](https://en.wikipedia.org/wiki/Single-page_application).
* If both `navigateFallbackDenylist` and `navigateFallbackAllowlist` are
* configured, the denylist takes precedent.
*
* *Note*: These RegExps may be evaluated against every destination URL during
* a navigation. Avoid using
* [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),
* or else your users may see delays when navigating your site.
*/
navigateFallbackAllowlist?: Array;
/**
* An optional array of regular expressions that restricts which URLs the
* configured `navigateFallback` behavior applies to. This is useful if only a
* subset of your site's URLs should be treated as being part of a
* [Single Page App](https://en.wikipedia.org/wiki/Single-page_application).
* If both `navigateFallbackDenylist` and `navigateFallbackAllowlist` are
* configured, the denylist takes precedence.
*
* *Note*: These RegExps may be evaluated against every destination URL during
* a navigation. Avoid using
* [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077),
* or else your users may see delays when navigating your site.
*/
navigateFallbackDenylist?: Array;
/**
* Whether or not to enable
* [navigation preload](https://developers.google.com/web/tools/workbox/modules/workbox-navigation-preload)
* in the generated service worker. When set to true, you must also use
* `runtimeCaching` to set up an appropriate response strategy that will match
* navigation requests, and make use of the preloaded response.
* @default false
*/
navigationPreload?: boolean;
/**
* Controls whether or not to include support for
* [offline Google Analytics](https://developers.google.com/web/tools/workbox/guides/enable-offline-analytics).
* When `true`, the call to `workbox-google-analytics`'s `initialize()` will
* be added to your generated service worker. When set to an `Object`, that
* object will be passed in to the `initialize()` call, allowing you to
* customize the behavior.
* @default false
*/
offlineGoogleAnalytics?: boolean | GoogleAnalyticsInitializeOptions;
/**
* When using Workbox's build tools to generate your service worker, you can
* specify one or more runtime caching configurations. These are then
* translated to {@link workbox-routing.registerRoute} calls using the match
* and handler configuration you define.
*
* For all of the options, see the {@link workbox-build.RuntimeCaching}
* documentation. The example below shows a typical configuration, with two
* runtime routes defined:
*
* @example
* runtimeCaching: [{
* urlPattern: ({url}) => url.origin === 'https://api.example.com',
* handler: 'NetworkFirst',
* options: {
* cacheName: 'api-cache',
* },
* }, {
* urlPattern: ({request}) => request.destination === 'image',
* handler: 'StaleWhileRevalidate',
* options: {
* cacheName: 'images-cache',
* expiration: {
* maxEntries: 10,
* },
* },
* }]
*/
runtimeCaching?: Array;
/**
* Whether to add an unconditional call to [`skipWaiting()`](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#skip_the_waiting_phase)
* to the generated service worker. If `false`, then a `message` listener will
* be added instead, allowing client pages to trigger `skipWaiting()` by
* calling `postMessage({type: 'SKIP_WAITING'})` on a waiting service worker.
* @default false
*/
skipWaiting?: boolean;
/**
* Whether to create a sourcemap for the generated service worker files.
* @default true
*/
sourcemap?: boolean;
}
// This needs to be set when using GetManifest or InjectManifest, but is
// optional when using GenerateSW if runtimeCaching is also used. This is
// enforced via runtime validation, and needs to be documented.
export interface RequiredGlobDirectoryPartial {
/**
* The local directory you wish to match `globPatterns` against. The path is
* relative to the current directory.
*/
globDirectory: string;
}
export interface OptionalGlobDirectoryPartial {
/**
* The local directory you wish to match `globPatterns` against. The path is
* relative to the current directory.
*/
globDirectory?: string;
}
export interface GlobPartial {
/**
* Determines whether or not symlinks are followed when generating the
* precache manifest. For more information, see the definition of `follow` in
* the `glob` [documentation](https://github.com/isaacs/node-glob#options).
* @default true
*/
globFollow?: boolean;
/**
* A set of patterns matching files to always exclude when generating the
* precache manifest. For more information, see the definition of `ignore` in
* the `glob` [documentation](https://github.com/isaacs/node-glob#options).
* @default ["**\/node_modules\/**\/*"]
*/
globIgnores?: Array;
/**
* Files matching any of these patterns will be included in the precache
* manifest. For more information, see the
* [`glob` primer](https://github.com/isaacs/node-glob#glob-primer).
* @default ["**\/*.{js,wasm,css,html}"]
*/
globPatterns?: Array;
/**
* If a URL is rendered based on some server-side logic, its contents may
* depend on multiple files or on some other unique string value. The keys in
* this object are server-rendered URLs. If the values are an array of
* strings, they will be interpreted as `glob` patterns, and the contents of
* any files matching the patterns will be used to uniquely version the URL.
* If used with a single string, it will be interpreted as unique versioning
* information that you've generated for a given URL.
*/
templatedURLs?: {
[key: string]: string | Array;
};
}
export interface InjectPartial {
/**
* The string to find inside of the `swSrc` file. Once found, it will be
* replaced by the generated precache manifest.
* @default "self.__WB_MANIFEST"
*/
injectionPoint?: string;
/**
* The path and filename of the service worker file that will be read during
* the build process, relative to the current working directory.
*/
swSrc: string;
}
export interface WebpackPartial {
/**
* One or more chunk names whose corresponding output files should be included
* in the precache manifest.
*/
chunks?: Array;
// We can't use the @default annotation here to assign the value via AJV, as
// an Array can't be serialized into JSON.
// The default value of [/\.map$/, /^manifest.*\.js$/] will be assigned by
// the validation function, and we need to reflect that in the docs.
/**
* One or more specifiers used to exclude assets from the precache manifest.
* This is interpreted following
* [the same rules](https://webpack.js.org/configuration/module/#condition)
* as `webpack`'s standard `exclude` option.
* If not provided, the default value is `[/\.map$/, /^manifest.*\.js$]`.
*/
//eslint-disable-next-line @typescript-eslint/ban-types
exclude?: Array boolean)>;
/**
* One or more chunk names whose corresponding output files should be excluded
* from the precache manifest.
*/
excludeChunks?: Array;
/**
* One or more specifiers used to include assets in the precache manifest.
* This is interpreted following
* [the same rules](https://webpack.js.org/configuration/module/#condition)
* as `webpack`'s standard `include` option.
*/
//eslint-disable-next-line @typescript-eslint/ban-types
include?: Array boolean)>;
/**
* If set to 'production', then an optimized service worker bundle that
* excludes debugging info will be produced. If not explicitly configured
* here, the `mode` value configured in the current `webpack` compilation
* will be used.
*/
mode?: string | null;
}
export interface RequiredSWDestPartial {
/**
* The path and filename of the service worker file that will be created by
* the build process, relative to the current working directory. It must end
* in '.js'.
*/
swDest: string;
}
export interface WebpackGenerateSWPartial {
/**
* One or more names of webpack chunks. The content of those chunks will be
* included in the generated service worker, via a call to `importScripts()`.
*/
importScriptsViaChunks?: Array;
/**
* The asset name of the service worker file created by this plugin.
* @default "service-worker.js"
*/
swDest?: string;
}
export interface WebpackInjectManifestPartial {
/**
* When `true` (the default), the `swSrc` file will be compiled by webpack.
* When `false`, compilation will not occur (and `webpackCompilationPlugins`
* can't be used.) Set to `false` if you want to inject the manifest into,
* e.g., a JSON file.
* @default true
*/
compileSrc?: boolean;
// This doesn't have a hardcoded default value; instead, the default will be
// set at runtime to the swSrc basename, with the hardcoded extension .js.
/**
* The asset name of the service worker file that will be created by this
* plugin. If omitted, the name will be based on the `swSrc` name.
*/
swDest?: string;
// This can only be set if compileSrc is true, but that restriction can't be
// represented in TypeScript. It's enforced via custom runtime validation
// logic and needs to be documented.
/**
* Optional `webpack` plugins that will be used when compiling the `swSrc`
* input file. Only valid if `compileSrc` is `true`.
*/
webpackCompilationPlugins?: Array;
}
export type GenerateSWOptions = BasePartial &
GlobPartial &
GeneratePartial &
RequiredSWDestPartial &
OptionalGlobDirectoryPartial;
export type GetManifestOptions = BasePartial &
GlobPartial &
RequiredGlobDirectoryPartial;
export type InjectManifestOptions = BasePartial &
GlobPartial &
InjectPartial &
RequiredSWDestPartial &
RequiredGlobDirectoryPartial;
export type WebpackGenerateSWOptions = BasePartial &
WebpackPartial &
GeneratePartial &
WebpackGenerateSWPartial;
export type WebpackInjectManifestOptions = BasePartial &
WebpackPartial &
InjectPartial &
WebpackInjectManifestPartial;
export interface GetManifestResult {
count: number;
manifestEntries: Array;
size: number;
warnings: Array;
}
export type BuildResult = Omit & {
filePaths: Array;
};
/**
* @private
*/
export interface FileDetails {
file: string;
hash: string;
size: number;
}
/**
* @private
*/
export type BuildType = 'dev' | 'prod';
/**
* @private
*/
export interface WorkboxPackageJSON extends PackageJson {
workbox?: {
browserNamespace?: string;
packageType?: string;
prodOnly?: boolean;
};
}
================================================
FILE: packages/workbox-build/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"esModuleInterop": true,
"module": "CommonJS",
"outDir": "./build",
"resolveJsonModule": true,
"rootDir": "./src",
"target": "ES2018",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"files": ["src/cdn-details.json"],
"include": ["src/**/*.ts", "src/schema/*.json"],
"references": [
{"path": "../workbox-background-sync/"},
{"path": "../workbox-broadcast-update/"},
{"path": "../workbox-cacheable-response/"},
{"path": "../workbox-core/"},
{"path": "../workbox-expiration/"},
{"path": "../workbox-google-analytics/"}
]
}
================================================
FILE: packages/workbox-cacheable-response/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-cacheable-response
================================================
FILE: packages/workbox-cacheable-response/package.json
================================================
{
"name": "workbox-cacheable-response",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "This library takes a Response object and determines whether it's cacheable based on a specific configuration.",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"workbox-plugin"
],
"workbox": {
"browserNamespace": "workbox.cacheableResponse",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"workbox-core": "7.4.0"
}
}
================================================
FILE: packages/workbox-cacheable-response/src/CacheableResponse.ts
================================================
/*
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 {assert} from 'workbox-core/_private/assert.js';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
import {logger} from 'workbox-core/_private/logger.js';
import './_version.js';
export interface CacheableResponseOptions {
statuses?: number[];
headers?: {[headerName: string]: string};
}
/**
* This class allows you to set up rules determining what
* status codes and/or headers need to be present in order for a
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* to be considered cacheable.
*
* @memberof workbox-cacheable-response
*/
class CacheableResponse {
private readonly _statuses?: CacheableResponseOptions['statuses'];
private readonly _headers?: CacheableResponseOptions['headers'];
/**
* To construct a new CacheableResponse instance you must provide at least
* one of the `config` properties.
*
* If both `statuses` and `headers` are specified, then both conditions must
* be met for the `Response` to be considered cacheable.
*
* @param {Object} config
* @param {Array} [config.statuses] One or more status codes that a
* `Response` can have and be considered cacheable.
* @param {Object} [config.headers] A mapping of header names
* and expected values that a `Response` can have and be considered cacheable.
* If multiple headers are provided, only one needs to be present.
*/
constructor(config: CacheableResponseOptions = {}) {
if (process.env.NODE_ENV !== 'production') {
if (!(config.statuses || config.headers)) {
throw new WorkboxError('statuses-or-headers-required', {
moduleName: 'workbox-cacheable-response',
className: 'CacheableResponse',
funcName: 'constructor',
});
}
if (config.statuses) {
assert!.isArray(config.statuses, {
moduleName: 'workbox-cacheable-response',
className: 'CacheableResponse',
funcName: 'constructor',
paramName: 'config.statuses',
});
}
if (config.headers) {
assert!.isType(config.headers, 'object', {
moduleName: 'workbox-cacheable-response',
className: 'CacheableResponse',
funcName: 'constructor',
paramName: 'config.headers',
});
}
}
this._statuses = config.statuses;
this._headers = config.headers;
}
/**
* Checks a response to see whether it's cacheable or not, based on this
* object's configuration.
*
* @param {Response} response The response whose cacheability is being
* checked.
* @return {boolean} `true` if the `Response` is cacheable, and `false`
* otherwise.
*/
isResponseCacheable(response: Response): boolean {
if (process.env.NODE_ENV !== 'production') {
assert!.isInstance(response, Response, {
moduleName: 'workbox-cacheable-response',
className: 'CacheableResponse',
funcName: 'isResponseCacheable',
paramName: 'response',
});
}
let cacheable = true;
if (this._statuses) {
cacheable = this._statuses.includes(response.status);
}
if (this._headers && cacheable) {
cacheable = Object.keys(this._headers).some((headerName) => {
return response.headers.get(headerName) === this._headers![headerName];
});
}
if (process.env.NODE_ENV !== 'production') {
if (!cacheable) {
logger.groupCollapsed(
`The request for ` +
`'${getFriendlyURL(response.url)}' returned a response that does ` +
`not meet the criteria for being cached.`,
);
logger.groupCollapsed(`View cacheability criteria here.`);
logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses));
logger.log(
`Cacheable headers: ` + JSON.stringify(this._headers, null, 2),
);
logger.groupEnd();
const logFriendlyHeaders: {[key: string]: string} = {};
response.headers.forEach((value, key) => {
logFriendlyHeaders[key] = value;
});
logger.groupCollapsed(`View response status and headers here.`);
logger.log(`Response status: ${response.status}`);
logger.log(
`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2),
);
logger.groupEnd();
logger.groupCollapsed(`View full response details here.`);
logger.log(response.headers);
logger.log(response);
logger.groupEnd();
logger.groupEnd();
}
}
return cacheable;
}
}
export {CacheableResponse};
================================================
FILE: packages/workbox-cacheable-response/src/CacheableResponsePlugin.ts
================================================
/*
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 {WorkboxPlugin} from 'workbox-core/types.js';
import {
CacheableResponse,
CacheableResponseOptions,
} from './CacheableResponse.js';
import './_version.js';
/**
* A class implementing the `cacheWillUpdate` lifecycle callback. This makes it
* easier to add in cacheability checks to requests made via Workbox's built-in
* strategies.
*
* @memberof workbox-cacheable-response
*/
class CacheableResponsePlugin implements WorkboxPlugin {
private readonly _cacheableResponse: CacheableResponse;
/**
* To construct a new CacheableResponsePlugin instance you must provide at
* least one of the `config` properties.
*
* If both `statuses` and `headers` are specified, then both conditions must
* be met for the `Response` to be considered cacheable.
*
* @param {Object} config
* @param {Array} [config.statuses] One or more status codes that a
* `Response` can have and be considered cacheable.
* @param {Object} [config.headers] A mapping of header names
* and expected values that a `Response` can have and be considered cacheable.
* If multiple headers are provided, only one needs to be present.
*/
constructor(config: CacheableResponseOptions) {
this._cacheableResponse = new CacheableResponse(config);
}
/**
* @param {Object} options
* @param {Response} options.response
* @return {Response|null}
* @private
*/
cacheWillUpdate: WorkboxPlugin['cacheWillUpdate'] = async ({response}) => {
if (this._cacheableResponse.isResponseCacheable(response)) {
return response;
}
return null;
};
}
export {CacheableResponsePlugin};
================================================
FILE: packages/workbox-cacheable-response/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:cacheable-response:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-cacheable-response/src/index.ts
================================================
/*
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 {
CacheableResponse,
CacheableResponseOptions,
} from './CacheableResponse.js';
import {CacheableResponsePlugin} from './CacheableResponsePlugin.js';
import './_version.js';
/**
* @module workbox-cacheable-response
*/
export {CacheableResponse, CacheableResponseOptions, CacheableResponsePlugin};
================================================
FILE: packages/workbox-cacheable-response/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"references": [{"path": "../workbox-core/"}]
}
================================================
FILE: packages/workbox-cli/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-cli
================================================
FILE: packages/workbox-cli/package.json
================================================
{
"name": "workbox-cli",
"version": "7.4.0",
"description": "workbox-cli is the command line interface for Workbox.",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"caching",
"fetch requests",
"offline",
"cli"
],
"bin": {
"workbox": "build/bin.js"
},
"files": [
"build"
],
"engines": {
"node": ">=20.0.0"
},
"author": "Google's Web DevRel Team and Google's Aurora Team",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox/tree/master/packages/workbox-cli",
"dependencies": {
"chalk": "^4.1.0",
"chokidar": "^3.6.0",
"common-tags": "^1.8.0",
"fs-extra": "^9.0.1",
"glob": "^11.0.1",
"inquirer": "^7.3.3",
"meow": "^7.1.0",
"ora": "^5.0.0",
"pretty-bytes": "^5.3.0",
"stringify-object": "^3.3.0",
"upath": "^1.2.0",
"update-notifier": "^7.3.1",
"workbox-build": "7.4.0"
},
"workbox": {
"packageType": "node_ts"
},
"devDependencies": {
"@types/common-tags": "^1.8.0",
"@types/fs-extra": "^9.0.1",
"@types/inquirer": "^9.0.3",
"@types/node": "^20.14.8",
"@types/stringify-object": "^3.3.0",
"@types/update-notifier": "^4.1.1"
}
}
================================================
FILE: packages/workbox-cli/src/app.ts
================================================
/*
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 * as workboxBuild from 'workbox-build';
import assert from 'assert';
import {default as chokidar, WatchOptions} from 'chokidar';
import meow from 'meow';
import prettyBytes from 'pretty-bytes';
import upath from 'upath';
import {constants} from './lib/constants.js';
import {errors} from './lib/errors.js';
import {logger} from './lib/logger.js';
import {readConfig} from './lib/read-config.js';
import {runWizard} from './lib/run-wizard.js';
import {SupportedFlags} from './bin.js';
interface BuildCommand {
command: 'generateSW' | 'injectManifest';
config: workboxBuild.GenerateSWOptions | workboxBuild.InjectManifestOptions;
watch: boolean;
}
/**
* Runs the specified build command with the provided configuration.
*
* @param {Object} options
*/
async function runBuildCommand({command, config, watch}: BuildCommand) {
const {count, filePaths, size, warnings} = await workboxBuild[command](
config as any,
);
for (const warning of warnings) {
logger.warn(warning);
}
if (filePaths.length === 1) {
logger.log(`The service worker file was written to ${config.swDest}`);
} else {
const message = filePaths
.sort()
.map((filePath) => ` • ${filePath}`)
.join(`\n`);
logger.log(`The service worker files were written to:\n${message}`);
}
logger.log(
`The service worker will precache ${count} URLs, ` +
`totaling ${prettyBytes(size)}.`,
);
if (watch) {
logger.log(`\nWatching for changes...`);
}
}
export const app = async (
params: meow.Result,
): Promise => {
// This should not be a user-visible error, unless meow() messes something up.
assert(params && Array.isArray(params.input), errors['missing-input']);
// Default to showing the help message if there's no command provided.
const [command = 'help', option] = params.input;
switch (command) {
case 'wizard': {
await runWizard(params.flags);
break;
}
case 'copyLibraries': {
assert(option, errors['missing-dest-dir-param']);
const parentDirectory = upath.resolve(process.cwd(), option);
const dirName = await workboxBuild.copyWorkboxLibraries(parentDirectory);
const fullPath = upath.join(parentDirectory, dirName);
logger.log(`The Workbox libraries were copied to ${fullPath}`);
logger.log(ol`Add a call to workbox.setConfig({modulePathPrefix: '...'})
to your service worker to use these local libraries.`);
logger.log(`See https://goo.gl/Fo9gPX for further documentation.`);
break;
}
case 'generateSW':
case 'injectManifest': {
const configPath = upath.resolve(
process.cwd(),
option || constants.defaultConfigFile,
);
let configFromDisk:
| workboxBuild.GenerateSWOptions
| workboxBuild.InjectManifestOptions;
try {
configFromDisk = readConfig(configPath);
} catch (error) {
if (error instanceof Error) {
logger.error(errors['invalid-common-js-module']);
throw error;
}
}
logger.log(`Using configuration from ${configPath}.`);
const config = configFromDisk!;
// Determine whether we're in --watch mode, or one-off mode.
if (params?.flags?.watch) {
const options: WatchOptions = {
ignoreInitial: true,
};
if (config.globIgnores) {
options.ignored = config.globIgnores;
}
if (config.globDirectory) {
options.cwd = config.globDirectory;
}
if (config.globPatterns) {
chokidar
.watch(config.globPatterns, options)
.on('all', async () => {
await runBuildCommand({command, config, watch: true});
})
.on('ready', async () => {
await runBuildCommand({command, config, watch: true});
})
.on('error', (err) => {
logger.error(err.toString());
});
}
} else {
await runBuildCommand({command, config, watch: false});
}
break;
}
case 'help': {
params.showHelp();
break;
}
default: {
throw new Error(errors['unknown-command'] + ` ` + command);
}
}
};
================================================
FILE: packages/workbox-cli/src/bin.ts
================================================
#! /usr/bin/env node
/*
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 meow from 'meow';
import updateNotifier from 'update-notifier';
import {app} from './app';
import {cleanupStackTrace} from './lib/cleanup-stack-trace.js';
import {helpText} from './lib/help-text';
import {logger} from './lib/logger';
export interface SupportedFlags extends meow.AnyFlags {
debug: meow.BooleanFlag;
injectManifest: meow.BooleanFlag;
watch: meow.BooleanFlag;
}
void (async () => {
const params: meow.Result = meow(helpText);
updateNotifier({pkg: params.pkg as updateNotifier.Package}).notify();
try {
await app(params);
} catch (error) {
if (error instanceof Error) {
// Show the full error and stack trace if we're run with --debug.
if (params.flags.debug) {
if (error.stack) {
logger.error(`\n${error.stack}`);
}
} else {
logger.error(`\n${error.message}`);
logger.debug(`${cleanupStackTrace(error, 'app.js')}\n`);
}
}
process.exit(1);
}
})();
================================================
FILE: packages/workbox-cli/src/lib/cleanup-stack-trace.ts
================================================
/*
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.
*/
// Helper to parse out less relevant info from an Error's stack trace.
// Removes the initial portion, since that's obtained from error.message.
// Removes every stack frame earlier than the last instance of moduleName,
// since that's just frames related to the Node runtime/loader.
export function cleanupStackTrace(error: Error, moduleName: string): string {
if (!error.stack) {
return '';
}
const frames = error.stack.split(`\n`);
let startFrame: number | undefined;
let lastFrame = 0;
frames.forEach((frame, index) => {
if (startFrame === undefined && frame.includes(` at `)) {
startFrame = index;
}
if (frame.includes(`${moduleName}:`)) {
lastFrame = index;
}
});
return frames.slice(startFrame, lastFrame + 1).join(`\n`);
}
================================================
FILE: packages/workbox-cli/src/lib/constants.ts
================================================
/*
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.
*/
export const constants = {
defaultConfigFile: 'workbox-config.js',
ignoredDirectories: ['node_modules'],
ignoredFileExtensions: ['map'],
ignoreURLParametersMatching: [/^utm_/, /^fbclid$/],
};
================================================
FILE: packages/workbox-cli/src/lib/errors.ts
================================================
/*
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 = {
'missing-input': `params.input value was not set properly.`,
'missing-dest-dir-param': ol`Please provide the path to a directory in which
the libraries will be copied.`,
'invalid-common-js-module': ol`Please pass in a valid CommonJS module that
exports your configuration.`,
'config-validation-failed': `Your configuration is invalid:`,
'workbox-build-runtime-error': `Service worker generation failed:`,
'unknown-command': `Unknown command:`,
'no-file-extensions-found': ol`No files could be found that are suitable for
caching.`,
'no-file-extensions-selected': `Please select at least one file extension.`,
'invalid-sw-dest': ol`Please enter a valid path to use for the service worker
file that's created.`,
'glob-directory-invalid': ol`The path you entered isn't a valid directory.`,
'invalid-config-location': ol`Please enter a valid path to use for the saved
configuration file.`,
'sw-src-missing-injection-point': ol`That is not a valid source service worker
file. Please try again with a file containing
'self.__WB_MANIFEST'.`,
'no-search-parameters-supplied': ol`Please provide the url search param(s)
you would like to ignore.`,
'invalid-search-parameters-supplied': ol`Please provide the valid URL search parameter(s)
without the leading '/' or '?' (i.e. source,version,language).`,
};
================================================
FILE: packages/workbox-cli/src/lib/help-text.ts
================================================
/*
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.
*/
export const helpText = `Usage:
$ workbox [options]
Commands:
wizard [--injectManifest]
Runs the configuration wizard, which will generate a
config file based on answers to questions.
By default the configuration will be tailored to the
generateSW use case.
If --injectManifest is provided, the wizard will ask
questions needed for the injectManifest use case.
generateSW [] [--watch]
Creates a new service worker file based on the options
in the config file (defaults to workbox-config.js).
If --watch is provided, the CLI will stay running, and will
rebuild the service worker each time a file in the precache
manifest changes.
See https://bit.ly/wb-generateSW
injectManifest [] [--watch]
Takes an existing service worker file and creates a
copy of it with a precache manifest "injected" into
it. The precache manifest is generated based on the
options in the config file (defaults to workbox-config.js).
If --watch is provided, the CLI will stay running, and will
rebuild the service worker each time a file in the precache
manifest changes.
See https://bit.ly/wb-injectManifest
copyLibraries
Makes a local copy of all of the Workbox libraries inside
a version directory at the location specified. This is intended
for developers using injectManifest who prefer using local,
rather than CDN hosted, libraries.
Config file:
In 'generateSW' or 'injectManifest' mode, the config file should be a
JavaScript file, in CommonJS module format.
By default, a config file named workbox-config.js in the current
directory is assumed, but this can be overridden.
Examples:
$ workbox wizard
$ workbox wizard --injectManifest
$ workbox generateSW --watch
$ workbox injectManifest configs/workbox-dev-config.js
$ workbox copyLibraries build/
`;
================================================
FILE: packages/workbox-cli/src/lib/logger.ts
================================================
/*
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 chalk from 'chalk';
export const logger = {
debug: (...args: string[]): void => console.log(chalk.gray(...args)),
log: (...args: string[]): void => console.log(...args),
warn: (...args: string[]): void => console.warn(chalk.yellow(...args)),
error: (...args: string[]): void => console.error(chalk.red.bold(...args)),
};
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-config-location.ts
================================================
/*
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 assert from 'assert';
import {Answers, Question} from 'inquirer';
import inquirer from 'inquirer';
import {oneLine as ol} from 'common-tags';
import {constants} from '../constants';
import {errors} from '../errors';
// The key used for the question/answer.
const name = 'configLocation';
const configLocationQuestion: Question = {
name,
message: ol`Where would you like to save these configuration options?`,
type: 'input',
default: constants.defaultConfigFile,
};
/**
* @return {Promise} The answers from inquirer.
*/
function askQuestion(): Promise {
return inquirer.prompt([configLocationQuestion]);
}
export async function askConfigLocation(): Promise {
const answers = await askQuestion();
// The value of the answer when the question type is 'input' is String
// and it has a default value, the casting is safe.
const configLocation: string = (answers[name] as string).trim();
assert(configLocation, errors['invalid-config-location']);
return configLocation;
}
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-extensions-to-cache.ts
================================================
/*
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 assert from 'assert';
import {Answers} from 'inquirer';
import inquirer from 'inquirer';
import {glob} from 'glob';
import ora from 'ora';
import upath from 'upath';
import {errors} from '../errors';
import {constants} from '../constants';
// The key used for the question/answer.
const name = 'globPatterns';
/**
* @param {string} globDirectory The directory used for the root of globbing.
* @return {Promise>} The unique file extensions corresponding
* to all of the files under globDirectory.
*/
async function getAllFileExtensions(globDirectory: string) {
const files = await glob('**/*.*', {
cwd: globDirectory,
nodir: true,
ignore: [
...constants.ignoredDirectories.map((directory) => `**/${directory}/**`),
...constants.ignoredFileExtensions.map(
(extension) => `**/*.${extension}`,
),
],
});
const extensions: Set = new Set();
for (const file of files) {
const extension = upath.extname(file);
if (extension) {
// Get rid of the leading . character.
extensions.add(extension.replace(/^\./, ''));
}
}
return [...extensions];
}
/**
* @param {string} globDirectory The directory used for the root of globbing.
* @return {Promise} The answers from inquirer.
*/
async function askQuestion(globDirectory: string): Promise {
// We need to get a list of extensions corresponding to files in the directory
// to use when asking the next question. That could potentially take some
// time, so we show a spinner and explanatory text.
const spinner = ora({
text: `Examining files in ${globDirectory}...`,
stream: process.stdout,
}).start();
const fileExtensions = await getAllFileExtensions(globDirectory);
spinner.stop();
assert(fileExtensions.length > 0, errors['no-file-extensions-found']);
return inquirer.prompt([
{
name,
message: 'Which file types would you like to precache?',
type: 'checkbox',
choices: fileExtensions,
default: fileExtensions,
},
]);
}
export async function askExtensionsToCache(
globDirectory: string,
): Promise {
const answers = await askQuestion(globDirectory);
// The return value is an array of strings with the selected values
// and there is a default, the casting is safe.
const extensions: string[] = answers[name] as string[];
assert(extensions.length > 0, errors['no-file-extensions-selected']);
// glob isn't happy with a single option inside of a {} group, so use a
// pattern without a {} group when there's only one extension.
const extensionsPattern: string =
extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
return [`**/*.${extensionsPattern}`];
}
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-questions.ts
================================================
/*
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 {askConfigLocation} from './ask-config-location';
import {askExtensionsToCache} from './ask-extensions-to-cache';
import {askRootOfWebApp} from './ask-root-of-web-app';
import {askSWDest} from './ask-sw-dest';
import {askSWSrc} from './ask-sw-src';
import {askQueryParametersInStartUrl} from './ask-start_url-query-params';
interface ConfigWithConfigLocation {
config: {
[key: string]: any;
};
configLocation: string;
}
export async function askQuestions(
options = {},
): Promise {
const isInjectManifest = 'injectManifest' in options;
const globDirectory = await askRootOfWebApp();
const globPatterns = await askExtensionsToCache(globDirectory);
const swSrc = isInjectManifest ? await askSWSrc() : undefined;
const swDest = await askSWDest(globDirectory);
const configLocation = await askConfigLocation();
// See https://github.com/GoogleChrome/workbox/issues/2985
const ignoreURLParametersMatching = isInjectManifest
? undefined
: await askQueryParametersInStartUrl();
const config: {[key: string]: any} = {
globDirectory,
globPatterns,
swDest,
};
if (swSrc) {
config.swSrc = swSrc;
}
if (ignoreURLParametersMatching) {
config.ignoreURLParametersMatching = ignoreURLParametersMatching;
}
return {
config,
configLocation,
};
}
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-root-of-web-app.ts
================================================
/*
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 assert from 'assert';
import fse from 'fs-extra';
import {glob} from 'glob';
import inquirer from 'inquirer';
import {oneLine as ol} from 'common-tags';
import {errors} from '../errors';
import {constants} from '../constants';
const ROOT_PROMPT = 'Please enter the path to the root of your web app:';
// The keys used for the questions/answers.
const questionRootDirectory = 'globDirectory';
const questionManualInput = 'manualDirectoryInput';
/**
* @return {Promise>} The subdirectories of the current
* working directory, with hidden and ignored ones filtered out.
*/
async function getSubdirectories(): Promise> {
return await glob('*/', {
ignore: constants.ignoredDirectories.map((directory) => `${directory}/`),
});
}
/**
* @return {Promise} The answers from inquirer.
*/
async function askQuestion(): Promise<{
globDirectory: string;
manualDirectoryInput?: string;
}> {
const subdirectories: (string | InstanceType)[] =
await getSubdirectories();
if (subdirectories.length > 0) {
const manualEntryChoice = 'Manually enter path';
return inquirer.prompt([
{
name: questionRootDirectory,
type: 'list',
message: ol`What is the root of your web app (i.e. which directory do
you deploy)?`,
choices: subdirectories.concat([
new inquirer.Separator(),
manualEntryChoice,
]),
},
{
name: questionManualInput,
when: (answers: {globDirectory: string}) =>
answers.globDirectory === manualEntryChoice,
message: ROOT_PROMPT,
},
]);
}
return inquirer.prompt([
{
name: questionRootDirectory,
message: ROOT_PROMPT,
default: '.',
},
]);
}
export async function askRootOfWebApp(): Promise {
const {manualDirectoryInput, globDirectory} = await askQuestion();
try {
const stat = await fse.stat(manualDirectoryInput || globDirectory);
assert(stat.isDirectory());
} catch (error) {
throw new Error(errors['glob-directory-invalid']);
}
return manualDirectoryInput || globDirectory;
}
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-start_url-query-params.ts
================================================
/*
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 inquirer from 'inquirer';
import {oneLine as ol} from 'common-tags';
import {errors} from '../errors';
import {constants} from '../constants';
const START_URL_QUERY_PARAMS_PROMPT =
'Please enter the search parameter(s) that you would like to ignore (separated by comma):';
// The keys used for the questions/answers.
const question_ignoreURLParametersMatching = 'ignoreURLParametersMatching';
const question_shouldAskForIgnoreURLParametersMatching =
'shouldAskForIgnoreURLParametersMatching';
/**
* @return {Promise} The answers from inquirer.
*/
async function askQuestion(): Promise<{
shouldAskForIgnoreURLParametersMatching: boolean;
ignoreURLParametersMatching?: string;
}> {
return inquirer.prompt([
{
name: question_shouldAskForIgnoreURLParametersMatching,
message: ol`Does your web app manifest include search parameter(s)
in the 'start_url', other than 'utm_' or 'fbclid'
(like '?source=pwa')?`,
type: 'confirm',
default: false,
},
{
name: question_ignoreURLParametersMatching,
when: (answer: {shouldAskForIgnoreURLParametersMatching: boolean}) =>
answer.shouldAskForIgnoreURLParametersMatching,
message: START_URL_QUERY_PARAMS_PROMPT,
type: 'input',
},
]);
}
export async function askQueryParametersInStartUrl(
defaultIgnoredSearchParameters: RegExp[] = constants.ignoreURLParametersMatching,
): Promise {
const {
shouldAskForIgnoreURLParametersMatching,
ignoreURLParametersMatching = '',
} = await askQuestion();
if (!shouldAskForIgnoreURLParametersMatching) {
return defaultIgnoredSearchParameters;
}
assert(
ignoreURLParametersMatching.length > 0,
errors['no-search-parameters-supplied'],
);
const ignoreSearchParameters = ignoreURLParametersMatching
.trim()
.split(',')
.filter(Boolean);
assert(
ignoreSearchParameters.length > 0,
errors['no-search-parameters-supplied'],
);
assert(
ignoreSearchParameters.every((param) => !param.match(/^[^\w|-]/g)),
errors['invalid-search-parameters-supplied'],
);
return defaultIgnoredSearchParameters.concat(
ignoreSearchParameters.map((searchParam) => new RegExp(`^${searchParam}`)),
);
}
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-sw-dest.ts
================================================
/*
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 assert from 'assert';
import {Answers} from 'inquirer';
import inquirer from 'inquirer';
import upath from 'upath';
import {errors} from '../errors';
// The key used for the question/answer.
const name = 'swDest';
/**
* @param {string} defaultDir
* @return {Promise} The answers from inquirer.
*/
function askQuestion(defaultDir: string): Promise {
return inquirer.prompt([
{
name,
message: `Where would you like your service worker file to be saved?`,
type: 'input',
default: upath.join(defaultDir, 'sw.js'),
},
]);
}
export async function askSWDest(defaultDir = '.'): Promise {
const answers = await askQuestion(defaultDir);
// When prompt type is input the return type is string
// casting is safe
const swDest: string = (answers[name] as string).trim();
assert(swDest, errors['invalid-sw-dest']);
return swDest;
}
================================================
FILE: packages/workbox-cli/src/lib/questions/ask-sw-src.ts
================================================
/*
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 {Answers} from 'inquirer';
import inquirer from 'inquirer';
import {oneLine as ol} from 'common-tags';
// The key used for the question/answer.
const name = 'swSrc';
/**
* @return {Promise} The answers from inquirer.
*/
function askQuestion(): Promise {
return inquirer.prompt([
{
name,
message: ol`Where's your existing service worker file? To be used with
injectManifest, it should include a call to
'self.__WB_MANIFEST'`,
type: 'input',
},
]);
}
export async function askSWSrc(): Promise {
const answers = await askQuestion();
// When prompt type is input the return is string or null
return answers[name] ? (answers[name] as string).trim() : null;
}
================================================
FILE: packages/workbox-cli/src/lib/read-config.ts
================================================
/*
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 {GenerateSWOptions, InjectManifestOptions} from 'workbox-build';
// A really light wrapper on top of Node's require() to make it easier to stub
// out reading the configuration during tests.
export function readConfig(
configFile: string,
): GenerateSWOptions | InjectManifestOptions {
return require(configFile) as GenerateSWOptions | InjectManifestOptions;
}
================================================
FILE: packages/workbox-cli/src/lib/run-wizard.ts
================================================
/*
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 {oneLine as ol} from 'common-tags';
import stringifyObject from 'stringify-object';
import {askQuestions} from './questions/ask-questions';
import {logger} from './logger';
export async function runWizard(options = {}): Promise {
const {configLocation, config} = await askQuestions(options);
// See https://github.com/GoogleChrome/workbox/issues/2796
const contents = `module.exports = ${stringifyObject(config)};`;
await fse.writeFile(configLocation, contents);
const command = 'injectManifest' in options ? 'injectManifest' : 'generateSW';
logger.log(`To build your service worker, run
workbox ${command} ${configLocation}
as part of a build process. See https://goo.gl/fdTQBf for details.`);
const configDocsURL =
'injectManifest' in options
? 'https://goo.gl/8bs14N'
: 'https://goo.gl/gVo87N';
logger.log(ol`You can further customize your service worker by making changes
to ${configLocation}. See ${configDocsURL} for details.`);
}
================================================
FILE: packages/workbox-cli/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"esModuleInterop": true,
"module": "CommonJS",
"outDir": "build",
"rootDir": "src",
"target": "ES2018",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"references": [{"path": "../workbox-build/"}]
}
================================================
FILE: packages/workbox-core/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-core
================================================
FILE: packages/workbox-core/package.json
================================================
{
"name": "workbox-core",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "This module is used by a number of the other Workbox modules to share common code.",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw"
],
"workbox": {
"browserNamespace": "workbox.core",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts"
}
================================================
FILE: packages/workbox-core/src/_private/Deferred.ts
================================================
/*
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 '../_version.js';
/**
* The Deferred class composes Promises in a way that allows for them to be
* resolved or rejected from outside the constructor. In most cases promises
* should be used directly, but Deferreds can be necessary when the logic to
* resolve a promise must be separate.
*
* @private
*/
class Deferred {
promise: Promise;
resolve!: (value: T) => void;
reject!: (reason?: any) => void;
/**
* Creates a promise and exposes its resolve and reject functions as methods.
*/
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
export {Deferred};
================================================
FILE: packages/workbox-core/src/_private/WorkboxError.ts
================================================
/*
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 {messageGenerator} from '../models/messages/messageGenerator.js';
import {MapLikeObject} from '../types.js';
import '../_version.js';
/**
* Workbox errors should be thrown with this class.
* This allows use to ensure the type easily in tests,
* helps developers identify errors from workbox
* easily and allows use to optimise error
* messages correctly.
*
* @private
*/
class WorkboxError extends Error {
details?: MapLikeObject;
/**
*
* @param {string} errorCode The error code that
* identifies this particular error.
* @param {Object=} details Any relevant arguments
* that will help developers identify issues should
* be added as a key on the context object.
*/
constructor(errorCode: string, details?: MapLikeObject) {
const message = messageGenerator(errorCode, details);
super(message);
this.name = errorCode;
this.details = details;
}
}
export {WorkboxError};
================================================
FILE: packages/workbox-core/src/_private/assert.ts
================================================
/*
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 {WorkboxError} from '../_private/WorkboxError.js';
import {MapLikeObject} from '../types.js';
import '../_version.js';
/*
* This method throws if the supplied value is not an array.
* The destructed values are required to produce a meaningful error for users.
* The destructed and restructured object is so it's clear what is
* needed.
*/
const isArray = (value: any[], details: MapLikeObject) => {
if (!Array.isArray(value)) {
throw new WorkboxError('not-an-array', details);
}
};
const hasMethod = (
object: MapLikeObject,
expectedMethod: string,
details: MapLikeObject,
) => {
const type = typeof object[expectedMethod];
if (type !== 'function') {
details['expectedMethod'] = expectedMethod;
throw new WorkboxError('missing-a-method', details);
}
};
const isType = (
object: unknown,
expectedType: string,
details: MapLikeObject,
) => {
if (typeof object !== expectedType) {
details['expectedType'] = expectedType;
throw new WorkboxError('incorrect-type', details);
}
};
const isInstance = (
object: unknown,
// Need the general type to do the check later.
// eslint-disable-next-line @typescript-eslint/ban-types
expectedClass: Function,
details: MapLikeObject,
) => {
if (!(object instanceof expectedClass)) {
details['expectedClassName'] = expectedClass.name;
throw new WorkboxError('incorrect-class', details);
}
};
const isOneOf = (value: any, validValues: any[], details: MapLikeObject) => {
if (!validValues.includes(value)) {
details['validValueDescription'] = `Valid values are ${JSON.stringify(
validValues,
)}.`;
throw new WorkboxError('invalid-value', details);
}
};
const isArrayOfClass = (
value: any,
// Need general type to do check later.
expectedClass: Function, // eslint-disable-line
details: MapLikeObject,
) => {
const error = new WorkboxError('not-array-of-class', details);
if (!Array.isArray(value)) {
throw error;
}
for (const item of value) {
if (!(item instanceof expectedClass)) {
throw error;
}
}
};
const finalAssertExports =
process.env.NODE_ENV === 'production'
? null
: {
hasMethod,
isArray,
isInstance,
isOneOf,
isType,
isArrayOfClass,
};
export {finalAssertExports as assert};
================================================
FILE: packages/workbox-core/src/_private/cacheMatchIgnoreParams.ts
================================================
/*
Copyright 2020 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 '../_version.js';
function stripParams(fullURL: string, ignoreParams: string[]) {
const strippedURL = new URL(fullURL);
for (const param of ignoreParams) {
strippedURL.searchParams.delete(param);
}
return strippedURL.href;
}
/**
* Matches an item in the cache, ignoring specific URL params. This is similar
* to the `ignoreSearch` option, but it allows you to ignore just specific
* params (while continuing to match on the others).
*
* @private
* @param {Cache} cache
* @param {Request} request
* @param {Object} matchOptions
* @param {Array} ignoreParams
* @return {Promise}
*/
async function cacheMatchIgnoreParams(
cache: Cache,
request: Request,
ignoreParams: string[],
matchOptions?: CacheQueryOptions,
): Promise {
const strippedRequestURL = stripParams(request.url, ignoreParams);
// If the request doesn't include any ignored params, match as normal.
if (request.url === strippedRequestURL) {
return cache.match(request, matchOptions);
}
// Otherwise, match by comparing keys
const keysOptions = {...matchOptions, ignoreSearch: true};
const cacheKeys = await cache.keys(request, keysOptions);
for (const cacheKey of cacheKeys) {
const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams);
if (strippedRequestURL === strippedCacheKeyURL) {
return cache.match(cacheKey, matchOptions);
}
}
return;
}
export {cacheMatchIgnoreParams};
================================================
FILE: packages/workbox-core/src/_private/cacheNames.ts
================================================
/*
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 '../_version.js';
declare let registration: ServiceWorkerRegistration | undefined;
export interface CacheNameDetails {
googleAnalytics: string;
precache: string;
prefix: string;
runtime: string;
suffix: string;
}
export interface PartialCacheNameDetails {
[propName: string]: string;
}
export type CacheNameDetailsProp =
| 'googleAnalytics'
| 'precache'
| 'prefix'
| 'runtime'
| 'suffix';
const _cacheNameDetails: CacheNameDetails = {
googleAnalytics: 'googleAnalytics',
precache: 'precache-v2',
prefix: 'workbox',
runtime: 'runtime',
suffix: typeof registration !== 'undefined' ? registration.scope : '',
};
const _createCacheName = (cacheName: string): string => {
return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix]
.filter((value) => value && value.length > 0)
.join('-');
};
const eachCacheNameDetail = (fn: (key: CacheNameDetailsProp) => void): void => {
for (const key of Object.keys(_cacheNameDetails)) {
fn(key as CacheNameDetailsProp);
}
};
export const cacheNames = {
updateDetails: (details: PartialCacheNameDetails): void => {
eachCacheNameDetail((key: CacheNameDetailsProp): void => {
if (typeof details[key] === 'string') {
_cacheNameDetails[key] = details[key];
}
});
},
getGoogleAnalyticsName: (userCacheName?: string): string => {
return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics);
},
getPrecacheName: (userCacheName?: string): string => {
return userCacheName || _createCacheName(_cacheNameDetails.precache);
},
getPrefix: (): string => {
return _cacheNameDetails.prefix;
},
getRuntimeName: (userCacheName?: string): string => {
return userCacheName || _createCacheName(_cacheNameDetails.runtime);
},
getSuffix: (): string => {
return _cacheNameDetails.suffix;
},
};
================================================
FILE: packages/workbox-core/src/_private/canConstructReadableStream.ts
================================================
/*
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 '../_version.js';
let supportStatus: boolean | undefined;
/**
* A utility function that determines whether the current browser supports
* constructing a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream)
* object.
*
* @return {boolean} `true`, if the current browser can successfully
* construct a `ReadableStream`, `false` otherwise.
*
* @private
*/
function canConstructReadableStream(): boolean {
if (supportStatus === undefined) {
// See https://github.com/GoogleChrome/workbox/issues/1473
try {
new ReadableStream({start() {}});
supportStatus = true;
} catch (error) {
supportStatus = false;
}
}
return supportStatus;
}
export {canConstructReadableStream};
================================================
FILE: packages/workbox-core/src/_private/canConstructResponseFromBodyStream.ts
================================================
/*
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 '../_version.js';
let supportStatus: boolean | undefined;
/**
* A utility function that determines whether the current browser supports
* constructing a new `Response` from a `response.body` stream.
*
* @return {boolean} `true`, if the current browser can successfully
* construct a `Response` from a `response.body` stream, `false` otherwise.
*
* @private
*/
function canConstructResponseFromBodyStream(): boolean {
if (supportStatus === undefined) {
const testResponse = new Response('');
if ('body' in testResponse) {
try {
new Response(testResponse.body);
supportStatus = true;
} catch (error) {
supportStatus = false;
}
}
supportStatus = false;
}
return supportStatus;
}
export {canConstructResponseFromBodyStream};
================================================
FILE: packages/workbox-core/src/_private/dontWaitFor.ts
================================================
/*
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 '../_version.js';
/**
* A helper function that prevents a promise from being flagged as unused.
*
* @private
**/
export function dontWaitFor(promise: Promise): void {
// Effective no-op.
void promise.then(() => {});
}
================================================
FILE: packages/workbox-core/src/_private/executeQuotaErrorCallbacks.ts
================================================
/*
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 {logger} from '../_private/logger.js';
import {quotaErrorCallbacks} from '../models/quotaErrorCallbacks.js';
import '../_version.js';
/**
* Runs all of the callback functions, one at a time sequentially, in the order
* in which they were registered.
*
* @memberof workbox-core
* @private
*/
async function executeQuotaErrorCallbacks(): Promise {
if (process.env.NODE_ENV !== 'production') {
logger.log(
`About to run ${quotaErrorCallbacks.size} ` +
`callbacks to clean up caches.`,
);
}
for (const callback of quotaErrorCallbacks) {
await callback();
if (process.env.NODE_ENV !== 'production') {
logger.log(callback, 'is complete.');
}
}
if (process.env.NODE_ENV !== 'production') {
logger.log('Finished running callbacks.');
}
}
export {executeQuotaErrorCallbacks};
================================================
FILE: packages/workbox-core/src/_private/getFriendlyURL.ts
================================================
/*
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 '../_version.js';
const getFriendlyURL = (url: URL | string): string => {
const urlObj = new URL(String(url), location.href);
// See https://github.com/GoogleChrome/workbox/issues/2323
// We want to include everything, except for the origin if it's same-origin.
return urlObj.href.replace(new RegExp(`^${location.origin}`), '');
};
export {getFriendlyURL};
================================================
FILE: packages/workbox-core/src/_private/logger.ts
================================================
/*
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 '../_version.js';
// logger is used inside of both service workers and the window global scope.
declare global {
interface WorkerGlobalScope {
__WB_DISABLE_DEV_LOGS: boolean;
}
interface Window {
__WB_DISABLE_DEV_LOGS: boolean;
}
}
type LoggerMethods =
| 'debug'
| 'log'
| 'warn'
| 'error'
| 'groupCollapsed'
| 'groupEnd';
const logger = (
process.env.NODE_ENV === 'production'
? null
: (() => {
// Don't overwrite this value if it's already set.
// See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923
if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) {
self.__WB_DISABLE_DEV_LOGS = false;
}
let inGroup = false;
const methodToColorMap: {[methodName: string]: string | null} = {
debug: `#7f8c8d`, // Gray
log: `#2ecc71`, // Green
warn: `#f39c12`, // Yellow
error: `#c0392b`, // Red
groupCollapsed: `#3498db`, // Blue
groupEnd: null, // No colored prefix on groupEnd
};
const print = function (method: LoggerMethods, args: any[]) {
if (self.__WB_DISABLE_DEV_LOGS) {
return;
}
if (method === 'groupCollapsed') {
// Safari doesn't print all console.groupCollapsed() arguments:
// https://bugs.webkit.org/show_bug.cgi?id=182754
if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
console[method](...args);
return;
}
}
const styles = [
`background: ${methodToColorMap[method]!}`,
`border-radius: 0.5em`,
`color: white`,
`font-weight: bold`,
`padding: 2px 0.5em`,
];
// When in a group, the workbox prefix is not displayed.
const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')];
console[method](...logPrefix, ...args);
if (method === 'groupCollapsed') {
inGroup = true;
}
if (method === 'groupEnd') {
inGroup = false;
}
};
// eslint-disable-next-line @typescript-eslint/ban-types
const api: {[methodName: string]: Function} = {};
const loggerMethods = Object.keys(methodToColorMap);
for (const key of loggerMethods) {
const method = key as LoggerMethods;
api[method] = (...args: any[]) => {
print(method, args);
};
}
return api as unknown;
})()
) as Console;
export {logger};
================================================
FILE: packages/workbox-core/src/_private/resultingClientExists.ts
================================================
/*
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 {timeout} from './timeout.js';
import '../_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
const MAX_RETRY_TIME = 2000;
/**
* Returns a promise that resolves to a window client matching the passed
* `resultingClientId`. For browsers that don't support `resultingClientId`
* or if waiting for the resulting client to apper takes too long, resolve to
* `undefined`.
*
* @param {string} [resultingClientId]
* @return {Promise}
* @private
*/
export async function resultingClientExists(
resultingClientId?: string,
): Promise {
if (!resultingClientId) {
return;
}
let existingWindows = await self.clients.matchAll({type: 'window'});
const existingWindowIds = new Set(existingWindows.map((w) => w.id));
let resultingWindow;
const startTime = performance.now();
// Only wait up to `MAX_RETRY_TIME` to find a matching client.
while (performance.now() - startTime < MAX_RETRY_TIME) {
existingWindows = await self.clients.matchAll({type: 'window'});
resultingWindow = existingWindows.find((w) => {
if (resultingClientId) {
// If we have a `resultingClientId`, we can match on that.
return w.id === resultingClientId;
} else {
// Otherwise match on finding a window not in `existingWindowIds`.
return !existingWindowIds.has(w.id);
}
});
if (resultingWindow) {
break;
}
// Sleep for 100ms and retry.
await timeout(100);
}
return resultingWindow;
}
================================================
FILE: packages/workbox-core/src/_private/timeout.ts
================================================
/*
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 '../_version.js';
/**
* Returns a promise that resolves and the passed number of milliseconds.
* This utility is an async/await-friendly version of `setTimeout`.
*
* @param {number} ms
* @return {Promise}
* @private
*/
export function timeout(ms: number): Promise {
return new Promise((resolve) => setTimeout(resolve, ms));
}
================================================
FILE: packages/workbox-core/src/_private/waitUntil.ts
================================================
/*
Copyright 2020 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 '../_version.js';
/**
* A utility method that makes it easier to use `event.waitUntil` with
* async functions and return the result.
*
* @param {ExtendableEvent} event
* @param {Function} asyncFn
* @return {Function}
* @private
*/
function waitUntil(
event: ExtendableEvent,
asyncFn: () => Promise,
): Promise {
const returnPromise = asyncFn();
event.waitUntil(returnPromise);
return returnPromise;
}
export {waitUntil};
================================================
FILE: packages/workbox-core/src/_private.ts
================================================
/*
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.
*/
// We either expose defaults or we expose every named export.
import {assert} from './_private/assert.js';
import {cacheNames} from './_private/cacheNames.js';
import {cacheMatchIgnoreParams} from './_private/cacheMatchIgnoreParams.js';
import {canConstructReadableStream} from './_private/canConstructReadableStream.js';
import {canConstructResponseFromBodyStream} from './_private/canConstructResponseFromBodyStream.js';
import {dontWaitFor} from './_private/dontWaitFor.js';
import {Deferred} from './_private/Deferred.js';
import {executeQuotaErrorCallbacks} from './_private/executeQuotaErrorCallbacks.js';
import {getFriendlyURL} from './_private/getFriendlyURL.js';
import {logger} from './_private/logger.js';
import {resultingClientExists} from './_private/resultingClientExists.js';
import {timeout} from './_private/timeout.js';
import {waitUntil} from './_private/waitUntil.js';
import {WorkboxError} from './_private/WorkboxError.js';
import './_version.js';
export {
assert,
cacheMatchIgnoreParams,
cacheNames,
canConstructReadableStream,
canConstructResponseFromBodyStream,
dontWaitFor,
Deferred,
executeQuotaErrorCallbacks,
getFriendlyURL,
logger,
resultingClientExists,
timeout,
waitUntil,
WorkboxError,
};
================================================
FILE: packages/workbox-core/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:core:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-core/src/cacheNames.ts
================================================
/*
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 {cacheNames as _cacheNames} from './_private/cacheNames.js';
import './_version.js';
/**
* Get the current cache names and prefix/suffix used by Workbox.
*
* `cacheNames.precache` is used for precached assets,
* `cacheNames.googleAnalytics` is used by `workbox-google-analytics` to
* store `analytics.js`, and `cacheNames.runtime` is used for everything else.
*
* `cacheNames.prefix` can be used to retrieve just the current prefix value.
* `cacheNames.suffix` can be used to retrieve just the current suffix value.
*
* @return {Object} An object with `precache`, `runtime`, `prefix`, and
* `googleAnalytics` properties.
*
* @memberof workbox-core
*/
const cacheNames = {
get googleAnalytics(): string {
return _cacheNames.getGoogleAnalyticsName();
},
get precache(): string {
return _cacheNames.getPrecacheName();
},
get prefix(): string {
return _cacheNames.getPrefix();
},
get runtime(): string {
return _cacheNames.getRuntimeName();
},
get suffix(): string {
return _cacheNames.getSuffix();
},
};
export {cacheNames};
================================================
FILE: packages/workbox-core/src/clientsClaim.ts
================================================
/*
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 './_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
/**
* Claim any currently available clients once the service worker
* becomes active. This is normally used in conjunction with `skipWaiting()`.
*
* @memberof workbox-core
*/
function clientsClaim(): void {
self.addEventListener('activate', () => self.clients.claim());
}
export {clientsClaim};
================================================
FILE: packages/workbox-core/src/copyResponse.ts
================================================
/*
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 {canConstructResponseFromBodyStream} from './_private/canConstructResponseFromBodyStream.js';
import {WorkboxError} from './_private/WorkboxError.js';
import './_version.js';
/**
* Allows developers to copy a response and modify its `headers`, `status`,
* or `statusText` values (the values settable via a
* [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax}
* object in the constructor).
* To modify these values, pass a function as the second argument. That
* function will be invoked with a single object with the response properties
* `{headers, status, statusText}`. The return value of this function will
* be used as the `ResponseInit` for the new `Response`. To change the values
* either modify the passed parameter(s) and return it, or return a totally
* new object.
*
* This method is intentionally limited to same-origin responses, regardless of
* whether CORS was used or not.
*
* @param {Response} response
* @param {Function} modifier
* @memberof workbox-core
*/
async function copyResponse(
response: Response,
modifier?: (responseInit: ResponseInit) => ResponseInit,
): Promise {
let origin = null;
// If response.url isn't set, assume it's cross-origin and keep origin null.
if (response.url) {
const responseURL = new URL(response.url);
origin = responseURL.origin;
}
if (origin !== self.location.origin) {
throw new WorkboxError('cross-origin-copy-response', {origin});
}
const clonedResponse = response.clone();
// Create a fresh `ResponseInit` object by cloning the headers.
const responseInit: ResponseInit = {
headers: new Headers(clonedResponse.headers),
status: clonedResponse.status,
statusText: clonedResponse.statusText,
};
// Apply any user modifications.
const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;
// Create the new response from the body stream and `ResponseInit`
// modifications. Note: not all browsers support the Response.body stream,
// so fall back to reading the entire body into memory as a blob.
const body = canConstructResponseFromBodyStream()
? clonedResponse.body
: await clonedResponse.blob();
return new Response(body, modifiedResponseInit);
}
export {copyResponse};
================================================
FILE: packages/workbox-core/src/index.ts
================================================
/*
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 {registerQuotaErrorCallback} from './registerQuotaErrorCallback.js';
import * as _private from './_private.js';
import {cacheNames} from './cacheNames.js';
import {copyResponse} from './copyResponse.js';
import {clientsClaim} from './clientsClaim.js';
import {setCacheNameDetails} from './setCacheNameDetails.js';
import {skipWaiting} from './skipWaiting.js';
import './_version.js';
/**
* All of the Workbox service worker libraries use workbox-core for shared
* code as well as setting default values that need to be shared (like cache
* names).
*
* @module workbox-core
*/
export {
_private,
cacheNames,
clientsClaim,
copyResponse,
registerQuotaErrorCallback,
setCacheNameDetails,
skipWaiting,
};
export * from './types.js';
================================================
FILE: packages/workbox-core/src/models/messages/messageGenerator.ts
================================================
/*
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 {messages} from './messages.js';
import '../../_version.js';
const fallback = (code: string, ...args: any[]) => {
let msg = code;
if (args.length > 0) {
msg += ` :: ${JSON.stringify(args)}`;
}
return msg;
};
const generatorFunction = (code: string, details = {}) => {
const message = messages[code];
if (!message) {
throw new Error(`Unable to find message for code '${code}'.`);
}
return message(details);
};
export const messageGenerator =
process.env.NODE_ENV === 'production' ? fallback : generatorFunction;
================================================
FILE: packages/workbox-core/src/models/messages/messages.ts
================================================
/*
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 '../../_version.js';
interface LoggableObject {
[key: string]: string | number;
}
interface MessageMap {
[messageID: string]: (param: LoggableObject) => string;
}
export const messages: MessageMap = {
'invalid-value': ({paramName, validValueDescription, value}) => {
if (!paramName || !validValueDescription) {
throw new Error(`Unexpected input to 'invalid-value' error.`);
}
return (
`The '${paramName}' parameter was given a value with an ` +
`unexpected value. ${validValueDescription} Received a value of ` +
`${JSON.stringify(value)}.`
);
},
'not-an-array': ({moduleName, className, funcName, paramName}) => {
if (!moduleName || !className || !funcName || !paramName) {
throw new Error(`Unexpected input to 'not-an-array' error.`);
}
return (
`The parameter '${paramName}' passed into ` +
`'${moduleName}.${className}.${funcName}()' must be an array.`
);
},
'incorrect-type': ({
expectedType,
paramName,
moduleName,
className,
funcName,
}) => {
if (!expectedType || !paramName || !moduleName || !funcName) {
throw new Error(`Unexpected input to 'incorrect-type' error.`);
}
const classNameStr = className ? `${className}.` : '';
return (
`The parameter '${paramName}' passed into ` +
`'${moduleName}.${classNameStr}` +
`${funcName}()' must be of type ${expectedType}.`
);
},
'incorrect-class': ({
expectedClassName,
paramName,
moduleName,
className,
funcName,
isReturnValueProblem,
}) => {
if (!expectedClassName || !moduleName || !funcName) {
throw new Error(`Unexpected input to 'incorrect-class' error.`);
}
const classNameStr = className ? `${className}.` : '';
if (isReturnValueProblem) {
return (
`The return value from ` +
`'${moduleName}.${classNameStr}${funcName}()' ` +
`must be an instance of class ${expectedClassName}.`
);
}
return (
`The parameter '${paramName}' passed into ` +
`'${moduleName}.${classNameStr}${funcName}()' ` +
`must be an instance of class ${expectedClassName}.`
);
},
'missing-a-method': ({
expectedMethod,
paramName,
moduleName,
className,
funcName,
}) => {
if (
!expectedMethod ||
!paramName ||
!moduleName ||
!className ||
!funcName
) {
throw new Error(`Unexpected input to 'missing-a-method' error.`);
}
return (
`${moduleName}.${className}.${funcName}() expected the ` +
`'${paramName}' parameter to expose a '${expectedMethod}' method.`
);
},
'add-to-cache-list-unexpected-type': ({entry}) => {
return (
`An unexpected entry was passed to ` +
`'workbox-precaching.PrecacheController.addToCacheList()' The entry ` +
`'${JSON.stringify(
entry,
)}' isn't supported. You must supply an array of ` +
`strings with one or more characters, objects with a url property or ` +
`Request objects.`
);
},
'add-to-cache-list-conflicting-entries': ({firstEntry, secondEntry}) => {
if (!firstEntry || !secondEntry) {
throw new Error(
`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`,
);
}
return (
`Two of the entries passed to ` +
`'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` +
`${firstEntry} but different revision details. Workbox is ` +
`unable to cache and version the asset correctly. Please remove one ` +
`of the entries.`
);
},
'plugin-error-request-will-fetch': ({thrownErrorMessage}) => {
if (!thrownErrorMessage) {
throw new Error(
`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`,
);
}
return (
`An error was thrown by a plugins 'requestWillFetch()' method. ` +
`The thrown error message was: '${thrownErrorMessage}'.`
);
},
'invalid-cache-name': ({cacheNameId, value}) => {
if (!cacheNameId) {
throw new Error(
`Expected a 'cacheNameId' for error 'invalid-cache-name'`,
);
}
return (
`You must provide a name containing at least one character for ` +
`setCacheDetails({${cacheNameId}: '...'}). Received a value of ` +
`'${JSON.stringify(value)}'`
);
},
'unregister-route-but-not-found-with-method': ({method}) => {
if (!method) {
throw new Error(
`Unexpected input to ` +
`'unregister-route-but-not-found-with-method' error.`,
);
}
return (
`The route you're trying to unregister was not previously ` +
`registered for the method type '${method}'.`
);
},
'unregister-route-route-not-registered': () => {
return (
`The route you're trying to unregister was not previously ` +
`registered.`
);
},
'queue-replay-failed': ({name}) => {
return `Replaying the background sync queue '${name}' failed.`;
},
'duplicate-queue-name': ({name}) => {
return (
`The Queue name '${name}' is already being used. ` +
`All instances of backgroundSync.Queue must be given unique names.`
);
},
'expired-test-without-max-age': ({methodName, paramName}) => {
return (
`The '${methodName}()' method can only be used when the ` +
`'${paramName}' is used in the constructor.`
);
},
'unsupported-route-type': ({moduleName, className, funcName, paramName}) => {
return (
`The supplied '${paramName}' parameter was an unsupported type. ` +
`Please check the docs for ${moduleName}.${className}.${funcName} for ` +
`valid input types.`
);
},
'not-array-of-class': ({
value,
expectedClass,
moduleName,
className,
funcName,
paramName,
}) => {
return (
`The supplied '${paramName}' parameter must be an array of ` +
`'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` +
`Please check the call to ${moduleName}.${className}.${funcName}() ` +
`to fix the issue.`
);
},
'max-entries-or-age-required': ({moduleName, className, funcName}) => {
return (
`You must define either config.maxEntries or config.maxAgeSeconds` +
`in ${moduleName}.${className}.${funcName}`
);
},
'statuses-or-headers-required': ({moduleName, className, funcName}) => {
return (
`You must define either config.statuses or config.headers` +
`in ${moduleName}.${className}.${funcName}`
);
},
'invalid-string': ({moduleName, funcName, paramName}) => {
if (!paramName || !moduleName || !funcName) {
throw new Error(`Unexpected input to 'invalid-string' error.`);
}
return (
`When using strings, the '${paramName}' parameter must start with ` +
`'http' (for cross-origin matches) or '/' (for same-origin matches). ` +
`Please see the docs for ${moduleName}.${funcName}() for ` +
`more info.`
);
},
'channel-name-required': () => {
return (
`You must provide a channelName to construct a ` +
`BroadcastCacheUpdate instance.`
);
},
'invalid-responses-are-same-args': () => {
return (
`The arguments passed into responsesAreSame() appear to be ` +
`invalid. Please ensure valid Responses are used.`
);
},
'expire-custom-caches-only': () => {
return (
`You must provide a 'cacheName' property when using the ` +
`expiration plugin with a runtime caching strategy.`
);
},
'unit-must-be-bytes': ({normalizedRangeHeader}) => {
if (!normalizedRangeHeader) {
throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`);
}
return (
`The 'unit' portion of the Range header must be set to 'bytes'. ` +
`The Range header provided was "${normalizedRangeHeader}"`
);
},
'single-range-only': ({normalizedRangeHeader}) => {
if (!normalizedRangeHeader) {
throw new Error(`Unexpected input to 'single-range-only' error.`);
}
return (
`Multiple ranges are not supported. Please use a single start ` +
`value, and optional end value. The Range header provided was ` +
`"${normalizedRangeHeader}"`
);
},
'invalid-range-values': ({normalizedRangeHeader}) => {
if (!normalizedRangeHeader) {
throw new Error(`Unexpected input to 'invalid-range-values' error.`);
}
return (
`The Range header is missing both start and end values. At least ` +
`one of those values is needed. The Range header provided was ` +
`"${normalizedRangeHeader}"`
);
},
'no-range-header': () => {
return `No Range header was found in the Request provided.`;
},
'range-not-satisfiable': ({size, start, end}) => {
return (
`The start (${start}) and end (${end}) values in the Range are ` +
`not satisfiable by the cached response, which is ${size} bytes.`
);
},
'attempt-to-cache-non-get-request': ({url, method}) => {
return (
`Unable to cache '${url}' because it is a '${method}' request and ` +
`only 'GET' requests can be cached.`
);
},
'cache-put-with-no-response': ({url}) => {
return (
`There was an attempt to cache '${url}' but the response was not ` +
`defined.`
);
},
'no-response': ({url, error}) => {
let message = `The strategy could not generate a response for '${url}'.`;
if (error) {
message += ` The underlying error is ${error}.`;
}
return message;
},
'bad-precaching-response': ({url, status}) => {
return (
`The precaching request for '${url}' failed` +
(status ? ` with an HTTP status of ${status}.` : `.`)
);
},
'non-precached-url': ({url}) => {
return (
`createHandlerBoundToURL('${url}') was called, but that URL is ` +
`not precached. Please pass in a URL that is precached instead.`
);
},
'add-to-cache-list-conflicting-integrities': ({url}) => {
return (
`Two of the entries passed to ` +
`'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` +
`${url} with different integrity values. Please remove one of them.`
);
},
'missing-precache-entry': ({cacheName, url}) => {
return `Unable to find a precached response in ${cacheName} for ${url}.`;
},
'cross-origin-copy-response': ({origin}) => {
return (
`workbox-core.copyResponse() can only be used with same-origin ` +
`responses. It was passed a response with origin ${origin}.`
);
},
'opaque-streams-source': ({type}) => {
const message =
`One of the workbox-streams sources resulted in an ` +
`'${type}' response.`;
if (type === 'opaqueredirect') {
return (
`${message} Please do not use a navigation request that results ` +
`in a redirect as a source.`
);
}
return `${message} Please ensure your sources are CORS-enabled.`;
},
};
================================================
FILE: packages/workbox-core/src/models/pluginEvents.ts
================================================
/*
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 '../_version.js';
export const enum pluginEvents {
CACHE_DID_UPDATE = 'cacheDidUpdate',
CACHE_KEY_WILL_BE_USED = 'cacheKeyWillBeUsed',
CACHE_WILL_UPDATE = 'cacheWillUpdate',
CACHED_RESPONSE_WILL_BE_USED = 'cachedResponseWillBeUsed',
FETCH_DID_FAIL = 'fetchDidFail',
FETCH_DID_SUCCEED = 'fetchDidSucceed',
REQUEST_WILL_FETCH = 'requestWillFetch',
}
================================================
FILE: packages/workbox-core/src/models/quotaErrorCallbacks.ts
================================================
/*
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 '../_version.js';
// Callbacks to be executed whenever there's a quota error.
// Can't change Function type right now.
// eslint-disable-next-line @typescript-eslint/ban-types
const quotaErrorCallbacks: Set = new Set();
export {quotaErrorCallbacks};
================================================
FILE: packages/workbox-core/src/registerQuotaErrorCallback.ts
================================================
/*
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 {logger} from './_private/logger.js';
import {assert} from './_private/assert.js';
import {quotaErrorCallbacks} from './models/quotaErrorCallbacks.js';
import './_version.js';
/**
* Adds a function to the set of quotaErrorCallbacks that will be executed if
* there's a quota error.
*
* @param {Function} callback
* @memberof workbox-core
*/
// Can't change Function type
// eslint-disable-next-line @typescript-eslint/ban-types
function registerQuotaErrorCallback(callback: Function): void {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(callback, 'function', {
moduleName: 'workbox-core',
funcName: 'register',
paramName: 'callback',
});
}
quotaErrorCallbacks.add(callback);
if (process.env.NODE_ENV !== 'production') {
logger.log('Registered a callback to respond to quota errors.', callback);
}
}
export {registerQuotaErrorCallback};
================================================
FILE: packages/workbox-core/src/setCacheNameDetails.ts
================================================
/*
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 {assert} from './_private/assert.js';
import {cacheNames, PartialCacheNameDetails} from './_private/cacheNames.js';
import {WorkboxError} from './_private/WorkboxError.js';
import './_version.js';
/**
* Modifies the default cache names used by the Workbox packages.
* Cache names are generated as `--`.
*
* @param {Object} details
* @param {Object} [details.prefix] The string to add to the beginning of
* the precache and runtime cache names.
* @param {Object} [details.suffix] The string to add to the end of
* the precache and runtime cache names.
* @param {Object} [details.precache] The cache name to use for precache
* caching.
* @param {Object} [details.runtime] The cache name to use for runtime caching.
* @param {Object} [details.googleAnalytics] The cache name to use for
* `workbox-google-analytics` caching.
*
* @memberof workbox-core
*/
function setCacheNameDetails(details: PartialCacheNameDetails): void {
if (process.env.NODE_ENV !== 'production') {
Object.keys(details).forEach((key) => {
assert!.isType(details[key], 'string', {
moduleName: 'workbox-core',
funcName: 'setCacheNameDetails',
paramName: `details.${key}`,
});
});
if ('precache' in details && details['precache']!.length === 0) {
throw new WorkboxError('invalid-cache-name', {
cacheNameId: 'precache',
value: details['precache'],
});
}
if ('runtime' in details && details['runtime']!.length === 0) {
throw new WorkboxError('invalid-cache-name', {
cacheNameId: 'runtime',
value: details['runtime'],
});
}
if (
'googleAnalytics' in details &&
details['googleAnalytics'].length === 0
) {
throw new WorkboxError('invalid-cache-name', {
cacheNameId: 'googleAnalytics',
value: details['googleAnalytics'],
});
}
}
cacheNames.updateDetails(details);
}
export {setCacheNameDetails};
================================================
FILE: packages/workbox-core/src/skipWaiting.ts
================================================
/*
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 {logger} from './_private/logger.js';
import './_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
/**
* This method is deprecated, and will be removed in Workbox v7.
*
* Calling self.skipWaiting() is equivalent, and should be used instead.
*
* @memberof workbox-core
*/
function skipWaiting(): void {
// Just call self.skipWaiting() directly.
// See https://github.com/GoogleChrome/workbox/issues/2525
if (process.env.NODE_ENV !== 'production') {
logger.warn(
`skipWaiting() from workbox-core is no longer recommended ` +
`and will be removed in Workbox v7. Using self.skipWaiting() instead ` +
`is equivalent.`,
);
}
void self.skipWaiting();
}
export {skipWaiting};
================================================
FILE: packages/workbox-core/src/types.ts
================================================
/*
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 './_version.js';
export interface MapLikeObject {
[key: string]: any;
}
/**
* Using a plain `MapLikeObject` for now, but could extend/restrict this
* in the future.
*/
export type PluginState = MapLikeObject;
/**
* Options passed to a `RouteMatchCallback` function.
*/
export interface RouteMatchCallbackOptions {
event: ExtendableEvent;
request: Request;
sameOrigin: boolean;
url: URL;
}
/**
* The "match" callback is used to determine if a `Route` should apply for a
* particular URL and request. When matching occurs in response to a fetch
* event from the client, the `event` object is also supplied. However, since
* the match callback can be invoked outside of a fetch event, matching logic
* should not assume the `event` object will always be available.
* If the match callback returns a truthy value, the matching route's
* `RouteHandlerCallback` will be invoked immediately. If the value returned
* is a non-empty array or object, that value will be set on the handler's
* `options.params` argument.
*/
export interface RouteMatchCallback {
(options: RouteMatchCallbackOptions): any;
}
/**
* Options passed to a `RouteHandlerCallback` function.
*/
export declare interface RouteHandlerCallbackOptions {
event: ExtendableEvent;
request: Request;
url: URL;
params?: string[] | MapLikeObject;
}
/**
* Options passed to a `ManualHandlerCallback` function.
*/
export interface ManualHandlerCallbackOptions {
event: ExtendableEvent;
request: Request | string;
}
export type HandlerCallbackOptions =
| RouteHandlerCallbackOptions
| ManualHandlerCallbackOptions;
/**
* The "handler" callback is invoked whenever a `Router` matches a URL/Request
* to a `Route` via its `RouteMatchCallback`. This handler callback should
* return a `Promise` that resolves with a `Response`.
*
* If a non-empty array or object is returned by the `RouteMatchCallback` it
* will be passed in as this handler's `options.params` argument.
*/
export interface RouteHandlerCallback {
(options: RouteHandlerCallbackOptions): Promise;
}
/**
* The "handler" callback is invoked whenever a `Router` matches a URL/Request
* to a `Route` via its `RouteMatchCallback`. This handler callback should
* return a `Promise` that resolves with a `Response`.
*
* If a non-empty array or object is returned by the `RouteMatchCallback` it
* will be passed in as this handler's `options.params` argument.
*/
export interface ManualHandlerCallback {
(options: ManualHandlerCallbackOptions): Promise;
}
/**
* An object with a `handle` method of type `RouteHandlerCallback`.
*
* A `Route` object can be created with either an `RouteHandlerCallback`
* function or this `RouteHandler` object. The benefit of the `RouteHandler`
* is it can be extended (as is done by the `workbox-strategies` package).
*/
export interface RouteHandlerObject {
handle: RouteHandlerCallback;
}
/**
* Either a `RouteHandlerCallback` or a `RouteHandlerObject`.
* Most APIs in `workbox-routing` that accept route handlers take either.
*/
export type RouteHandler = RouteHandlerCallback | RouteHandlerObject;
export interface HandlerWillStartCallbackParam {
request: Request;
event: ExtendableEvent;
state?: PluginState;
}
export interface HandlerWillStartCallback {
(param: HandlerWillStartCallbackParam): Promise;
}
export interface CacheDidUpdateCallbackParam {
cacheName: string;
newResponse: Response;
request: Request;
event: ExtendableEvent;
oldResponse?: Response | null;
state?: PluginState;
}
export interface CacheDidUpdateCallback {
(param: CacheDidUpdateCallbackParam): Promise;
}
export interface CacheKeyWillBeUsedCallbackParam {
mode: string;
request: Request;
event: ExtendableEvent;
params?: any;
state?: PluginState;
}
export interface CacheKeyWillBeUsedCallback {
(param: CacheKeyWillBeUsedCallbackParam): Promise;
}
export interface CacheWillUpdateCallbackParam {
request: Request;
response: Response;
event: ExtendableEvent;
state?: PluginState;
}
export interface CacheWillUpdateCallback {
(param: CacheWillUpdateCallbackParam): Promise<
Response | void | null | undefined
>;
}
export interface CachedResponseWillBeUsedCallbackParam {
cacheName: string;
request: Request;
cachedResponse?: Response;
event: ExtendableEvent;
matchOptions?: CacheQueryOptions;
state?: PluginState;
}
export interface CachedResponseWillBeUsedCallback {
(param: CachedResponseWillBeUsedCallbackParam): Promise<
Response | void | null | undefined
>;
}
export interface FetchDidFailCallbackParam {
error: Error;
originalRequest: Request;
request: Request;
event: ExtendableEvent;
state?: PluginState;
}
export interface FetchDidFailCallback {
(param: FetchDidFailCallbackParam): Promise;
}
export interface FetchDidSucceedCallbackParam {
request: Request;
response: Response;
event: ExtendableEvent;
state?: PluginState;
}
export interface FetchDidSucceedCallback {
(param: FetchDidSucceedCallbackParam): Promise;
}
export interface RequestWillFetchCallbackParam {
request: Request;
event: ExtendableEvent;
state?: PluginState;
}
export interface RequestWillFetchCallback {
(param: RequestWillFetchCallbackParam): Promise;
}
export interface HandlerWillRespondCallbackParam {
request: Request;
response: Response;
event: ExtendableEvent;
state?: PluginState;
}
export interface HandlerWillRespondCallback {
(param: HandlerWillRespondCallbackParam): Promise;
}
export interface HandlerDidErrorCallbackParam {
request: Request;
event: ExtendableEvent;
error: Error;
state?: PluginState;
}
export interface HandlerDidErrorCallback {
(param: HandlerDidErrorCallbackParam): Promise;
}
export interface HandlerDidRespondCallbackParam {
request: Request;
event: ExtendableEvent;
response?: Response;
state?: PluginState;
}
export interface HandlerDidRespondCallback {
(param: HandlerDidRespondCallbackParam): Promise;
}
export interface HandlerDidCompleteCallbackParam {
request: Request;
error?: Error;
event: ExtendableEvent;
response?: Response;
state?: PluginState;
}
export interface HandlerDidCompleteCallback {
(param: HandlerDidCompleteCallbackParam): Promise;
}
/**
* An object with optional lifecycle callback properties for the fetch and
* cache operations.
*/
export declare interface WorkboxPlugin {
cacheDidUpdate?: CacheDidUpdateCallback;
cachedResponseWillBeUsed?: CachedResponseWillBeUsedCallback;
cacheKeyWillBeUsed?: CacheKeyWillBeUsedCallback;
cacheWillUpdate?: CacheWillUpdateCallback;
fetchDidFail?: FetchDidFailCallback;
fetchDidSucceed?: FetchDidSucceedCallback;
handlerDidComplete?: HandlerDidCompleteCallback;
handlerDidError?: HandlerDidErrorCallback;
handlerDidRespond?: HandlerDidRespondCallback;
handlerWillRespond?: HandlerWillRespondCallback;
handlerWillStart?: HandlerWillStartCallback;
requestWillFetch?: RequestWillFetchCallback;
}
export interface WorkboxPluginCallbackParam {
cacheDidUpdate: CacheDidUpdateCallbackParam;
cachedResponseWillBeUsed: CachedResponseWillBeUsedCallbackParam;
cacheKeyWillBeUsed: CacheKeyWillBeUsedCallbackParam;
cacheWillUpdate: CacheWillUpdateCallbackParam;
fetchDidFail: FetchDidFailCallbackParam;
fetchDidSucceed: FetchDidSucceedCallbackParam;
handlerDidComplete: HandlerDidCompleteCallbackParam;
handlerDidError: HandlerDidErrorCallbackParam;
handlerDidRespond: HandlerDidRespondCallbackParam;
handlerWillRespond: HandlerWillRespondCallbackParam;
handlerWillStart: HandlerWillStartCallbackParam;
requestWillFetch: RequestWillFetchCallbackParam;
}
================================================
FILE: packages/workbox-core/src/utils/pluginUtils.ts
================================================
/*
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 {WorkboxPlugin} from '../types.js';
import '../_version.js';
export const pluginUtils = {
filter: (plugins: WorkboxPlugin[], callbackName: string): WorkboxPlugin[] => {
return plugins.filter((plugin) => callbackName in plugin);
},
};
================================================
FILE: packages/workbox-core/src/utils/welcome.ts
================================================
/*
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 {logger} from '../_private/logger.js';
import '../_version.js';
// A WorkboxCore instance must be exported before we can use the logger.
// This is so it can get the current log level.
if (process.env.NODE_ENV !== 'production') {
const padding = ' ';
logger.groupCollapsed('Welcome to Workbox!');
logger.log(
`You are currently using a development build. ` +
`By default this will switch to prod builds when not on localhost. ` +
`You can force this with workbox.setConfig({debug: true|false}).`,
);
logger.log(
`📖 Read the guides and documentation\n` +
`${padding}https://developers.google.com/web/tools/workbox/`,
);
logger.log(
`❓ Use the [workbox] tag on Stack Overflow to ask questions\n` +
`${padding}https://stackoverflow.com/questions/ask?tags=workbox`,
);
logger.log(
`🐛 Found a bug? Report it on GitHub\n` +
`${padding}https://github.com/GoogleChrome/workbox/issues/new`,
);
logger.groupEnd();
}
================================================
FILE: packages/workbox-core/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"]
}
================================================
FILE: packages/workbox-expiration/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-expiration
================================================
FILE: packages/workbox-expiration/package.json
================================================
{
"name": "workbox-expiration",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "A service worker helper library that expires cached responses based on age or maximum number of entries.",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"workbox-plugin"
],
"workbox": {
"browserNamespace": "workbox.expiration",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"idb": "^7.0.1",
"workbox-core": "7.4.0"
}
}
================================================
FILE: packages/workbox-expiration/src/CacheExpiration.ts
================================================
/*
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 {assert} from 'workbox-core/_private/assert.js';
import {dontWaitFor} from 'workbox-core/_private/dontWaitFor.js';
import {logger} from 'workbox-core/_private/logger.js';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {CacheTimestampsModel} from './models/CacheTimestampsModel.js';
import './_version.js';
interface CacheExpirationConfig {
maxEntries?: number;
maxAgeSeconds?: number;
matchOptions?: CacheQueryOptions;
}
/**
* The `CacheExpiration` class allows you define an expiration and / or
* limit on the number of responses stored in a
* [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
*
* @memberof workbox-expiration
*/
class CacheExpiration {
private _isRunning = false;
private _rerunRequested = false;
private readonly _maxEntries?: number;
private readonly _maxAgeSeconds?: number;
private readonly _matchOptions?: CacheQueryOptions;
private readonly _cacheName: string;
private readonly _timestampModel: CacheTimestampsModel;
/**
* To construct a new CacheExpiration instance you must provide at least
* one of the `config` properties.
*
* @param {string} cacheName Name of the cache to apply restrictions to.
* @param {Object} config
* @param {number} [config.maxEntries] The maximum number of entries to cache.
* Entries used the least will be removed as the maximum is reached.
* @param {number} [config.maxAgeSeconds] The maximum age of an entry before
* it's treated as stale and removed.
* @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters)
* that will be used when calling `delete()` on the cache.
*/
constructor(cacheName: string, config: CacheExpirationConfig = {}) {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'cacheName',
});
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
});
}
if (config.maxEntries) {
assert!.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxEntries',
});
}
if (config.maxAgeSeconds) {
assert!.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds',
});
}
}
this._maxEntries = config.maxEntries;
this._maxAgeSeconds = config.maxAgeSeconds;
this._matchOptions = config.matchOptions;
this._cacheName = cacheName;
this._timestampModel = new CacheTimestampsModel(cacheName);
}
/**
* Expires entries for the given cache and given criteria.
*/
async expireEntries(): Promise {
if (this._isRunning) {
this._rerunRequested = true;
return;
}
this._isRunning = true;
const minTimestamp = this._maxAgeSeconds
? Date.now() - this._maxAgeSeconds * 1000
: 0;
const urlsExpired = await this._timestampModel.expireEntries(
minTimestamp,
this._maxEntries,
);
// Delete URLs from the cache
const cache = await self.caches.open(this._cacheName);
for (const url of urlsExpired) {
await cache.delete(url, this._matchOptions);
}
if (process.env.NODE_ENV !== 'production') {
if (urlsExpired.length > 0) {
logger.groupCollapsed(
`Expired ${urlsExpired.length} ` +
`${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` +
`${urlsExpired.length === 1 ? 'it' : 'them'} from the ` +
`'${this._cacheName}' cache.`,
);
logger.log(
`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`,
);
urlsExpired.forEach((url) => logger.log(` ${url}`));
logger.groupEnd();
} else {
logger.debug(`Cache expiration ran and found no entries to remove.`);
}
}
this._isRunning = false;
if (this._rerunRequested) {
this._rerunRequested = false;
dontWaitFor(this.expireEntries());
}
}
/**
* Update the timestamp for the given URL. This ensures the when
* removing entries based on maximum entries, most recently used
* is accurate or when expiring, the timestamp is up-to-date.
*
* @param {string} url
*/
async updateTimestamp(url: string): Promise {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(url, 'string', {
moduleName: 'workbox-expiration',
className: 'CacheExpiration',
funcName: 'updateTimestamp',
paramName: 'url',
});
}
await this._timestampModel.setTimestamp(url, Date.now());
}
/**
* Can be used to check if a URL has expired or not before it's used.
*
* This requires a look up from IndexedDB, so can be slow.
*
* Note: This method will not remove the cached entry, call
* `expireEntries()` to remove indexedDB and Cache entries.
*
* @param {string} url
* @return {boolean}
*/
async isURLExpired(url: string): Promise {
if (!this._maxAgeSeconds) {
if (process.env.NODE_ENV !== 'production') {
throw new WorkboxError(`expired-test-without-max-age`, {
methodName: 'isURLExpired',
paramName: 'maxAgeSeconds',
});
}
return false;
} else {
const timestamp = await this._timestampModel.getTimestamp(url);
const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000;
return timestamp !== undefined ? timestamp < expireOlderThan : true;
}
}
/**
* Removes the IndexedDB object store used to keep track of cache expiration
* metadata.
*/
async delete(): Promise {
// Make sure we don't attempt another rerun if we're called in the middle of
// a cache expiration.
this._rerunRequested = false;
await this._timestampModel.expireEntries(Infinity); // Expires all.
}
}
export {CacheExpiration};
================================================
FILE: packages/workbox-expiration/src/ExpirationPlugin.ts
================================================
/*
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 {assert} from 'workbox-core/_private/assert.js';
import {cacheNames} from 'workbox-core/_private/cacheNames.js';
import {dontWaitFor} from 'workbox-core/_private/dontWaitFor.js';
import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
import {logger} from 'workbox-core/_private/logger.js';
import {registerQuotaErrorCallback} from 'workbox-core/registerQuotaErrorCallback.js';
import {WorkboxError} from 'workbox-core/_private/WorkboxError.js';
import {WorkboxPlugin} from 'workbox-core/types.js';
import {CacheExpiration} from './CacheExpiration.js';
import './_version.js';
export interface ExpirationPluginOptions {
maxEntries?: number;
maxAgeSeconds?: number;
matchOptions?: CacheQueryOptions;
purgeOnQuotaError?: boolean;
}
/**
* This plugin can be used in a `workbox-strategy` to regularly enforce a
* limit on the age and / or the number of cached requests.
*
* It can only be used with `workbox-strategy` instances that have a
* [custom `cacheName` property set](/web/tools/workbox/guides/configure-workbox#custom_cache_names_in_strategies).
* In other words, it can't be used to expire entries in strategy that uses the
* default runtime cache name.
*
* Whenever a cached response is used or updated, this plugin will look
* at the associated cache and remove any old or extra responses.
*
* When using `maxAgeSeconds`, responses may be used *once* after expiring
* because the expiration clean up will not have occurred until *after* the
* cached response has been used. If the response has a "Date" header, then
* a light weight expiration check is performed and the response will not be
* used immediately.
*
* When using `maxEntries`, the entry least-recently requested will be removed
* from the cache first.
*
* @memberof workbox-expiration
*/
class ExpirationPlugin implements WorkboxPlugin {
private readonly _config: ExpirationPluginOptions;
private readonly _maxAgeSeconds?: number;
private _cacheExpirations: Map;
/**
* @param {ExpirationPluginOptions} config
* @param {number} [config.maxEntries] The maximum number of entries to cache.
* Entries used the least will be removed as the maximum is reached.
* @param {number} [config.maxAgeSeconds] The maximum age of an entry before
* it's treated as stale and removed.
* @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters)
* that will be used when calling `delete()` on the cache.
* @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to
* automatic deletion if the available storage quota has been exceeded.
*/
constructor(config: ExpirationPluginOptions = {}) {
if (process.env.NODE_ENV !== 'production') {
if (!(config.maxEntries || config.maxAgeSeconds)) {
throw new WorkboxError('max-entries-or-age-required', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
});
}
if (config.maxEntries) {
assert!.isType(config.maxEntries, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxEntries',
});
}
if (config.maxAgeSeconds) {
assert!.isType(config.maxAgeSeconds, 'number', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'constructor',
paramName: 'config.maxAgeSeconds',
});
}
}
this._config = config;
this._maxAgeSeconds = config.maxAgeSeconds;
this._cacheExpirations = new Map();
if (config.purgeOnQuotaError) {
registerQuotaErrorCallback(() => this.deleteCacheAndMetadata());
}
}
/**
* A simple helper method to return a CacheExpiration instance for a given
* cache name.
*
* @param {string} cacheName
* @return {CacheExpiration}
*
* @private
*/
private _getCacheExpiration(cacheName: string): CacheExpiration {
if (cacheName === cacheNames.getRuntimeName()) {
throw new WorkboxError('expire-custom-caches-only');
}
let cacheExpiration = this._cacheExpirations.get(cacheName);
if (!cacheExpiration) {
cacheExpiration = new CacheExpiration(cacheName, this._config);
this._cacheExpirations.set(cacheName, cacheExpiration);
}
return cacheExpiration;
}
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` handlers when a `Response` is about to be returned
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
* the handler. It allows the `Response` to be inspected for freshness and
* prevents it from being used if the `Response`'s `Date` header value is
* older than the configured `maxAgeSeconds`.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache the response is in.
* @param {Response} options.cachedResponse The `Response` object that's been
* read from a cache and whose freshness should be checked.
* @return {Response} Either the `cachedResponse`, if it's
* fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
*
* @private
*/
cachedResponseWillBeUsed: WorkboxPlugin['cachedResponseWillBeUsed'] = async ({
event,
request,
cacheName,
cachedResponse,
}) => {
if (!cachedResponse) {
return null;
}
const isFresh = this._isResponseDateFresh(cachedResponse);
// Expire entries to ensure that even if the expiration date has
// expired, it'll only be used once.
const cacheExpiration = this._getCacheExpiration(cacheName);
dontWaitFor(cacheExpiration.expireEntries());
// Update the metadata for the request URL to the current timestamp,
// but don't `await` it as we don't want to block the response.
const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
if (event) {
try {
event.waitUntil(updateTimestampDone);
} catch (error) {
if (process.env.NODE_ENV !== 'production') {
// The event may not be a fetch event; only log the URL if it is.
if ('request' in event) {
logger.warn(
`Unable to ensure service worker stays alive when ` +
`updating cache entry for ` +
`'${getFriendlyURL((event as FetchEvent).request.url)}'.`,
);
}
}
}
}
return isFresh ? cachedResponse : null;
};
/**
* @param {Response} cachedResponse
* @return {boolean}
*
* @private
*/
private _isResponseDateFresh(cachedResponse: Response): boolean {
if (!this._maxAgeSeconds) {
// We aren't expiring by age, so return true, it's fresh
return true;
}
// Check if the 'date' header will suffice a quick expiration check.
// See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
// discussion.
const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
if (dateHeaderTimestamp === null) {
// Unable to parse date, so assume it's fresh.
return true;
}
// If we have a valid headerTime, then our response is fresh iff the
// headerTime plus maxAgeSeconds is greater than the current time.
const now = Date.now();
return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
}
/**
* This method will extract the data header and parse it into a useful
* value.
*
* @param {Response} cachedResponse
* @return {number|null}
*
* @private
*/
private _getDateHeaderTimestamp(cachedResponse: Response): number | null {
if (!cachedResponse.headers.has('date')) {
return null;
}
const dateHeader = cachedResponse.headers.get('date');
const parsedDate = new Date(dateHeader!);
const headerTime = parsedDate.getTime();
// If the Date header was invalid for some reason, parsedDate.getTime()
// will return NaN.
if (isNaN(headerTime)) {
return null;
}
return headerTime;
}
/**
* A "lifecycle" callback that will be triggered automatically by the
* `workbox-strategies` handlers when an entry is added to a cache.
*
* @param {Object} options
* @param {string} options.cacheName Name of the cache that was updated.
* @param {string} options.request The Request for the cached entry.
*
* @private
*/
cacheDidUpdate: WorkboxPlugin['cacheDidUpdate'] = async ({
cacheName,
request,
}) => {
if (process.env.NODE_ENV !== 'production') {
assert!.isType(cacheName, 'string', {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'cacheName',
});
assert!.isInstance(request, Request, {
moduleName: 'workbox-expiration',
className: 'Plugin',
funcName: 'cacheDidUpdate',
paramName: 'request',
});
}
const cacheExpiration = this._getCacheExpiration(cacheName);
await cacheExpiration.updateTimestamp(request.url);
await cacheExpiration.expireEntries();
};
/**
* This is a helper method that performs two operations:
*
* - Deletes *all* the underlying Cache instances associated with this plugin
* instance, by calling caches.delete() on your behalf.
* - Deletes the metadata from IndexedDB used to keep track of expiration
* details for each Cache instance.
*
* When using cache expiration, calling this method is preferable to calling
* `caches.delete()` directly, since this will ensure that the IndexedDB
* metadata is also cleanly removed and open IndexedDB instances are deleted.
*
* Note that if you're *not* using cache expiration for a given cache, calling
* `caches.delete()` and passing in the cache's name should be sufficient.
* There is no Workbox-specific method needed for cleanup in that case.
*/
async deleteCacheAndMetadata(): Promise {
// Do this one at a time instead of all at once via `Promise.all()` to
// reduce the chance of inconsistency if a promise rejects.
for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
await self.caches.delete(cacheName);
await cacheExpiration.delete();
}
// Reset this._cacheExpirations to its initial state.
this._cacheExpirations = new Map();
}
}
export {ExpirationPlugin};
================================================
FILE: packages/workbox-expiration/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:expiration:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-expiration/src/index.ts
================================================
/*
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 {CacheExpiration} from './CacheExpiration.js';
import {ExpirationPlugin, ExpirationPluginOptions} from './ExpirationPlugin.js';
import './_version.js';
/**
* @module workbox-expiration
*/
export {CacheExpiration, ExpirationPlugin, ExpirationPluginOptions};
================================================
FILE: packages/workbox-expiration/src/models/CacheTimestampsModel.ts
================================================
/*
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 {openDB, DBSchema, IDBPDatabase, deleteDB} from 'idb';
import '../_version.js';
const DB_NAME = 'workbox-expiration';
const CACHE_OBJECT_STORE = 'cache-entries';
const normalizeURL = (unNormalizedUrl: string) => {
const url = new URL(unNormalizedUrl, location.href);
url.hash = '';
return url.href;
};
interface CacheTimestampsModelEntry {
id: string;
cacheName: string;
url: string;
timestamp: number;
}
interface CacheDbSchema extends DBSchema {
'cache-entries': {
key: string;
value: CacheTimestampsModelEntry;
indexes: {cacheName: string; timestamp: number};
};
}
/**
* Returns the timestamp model.
*
* @private
*/
class CacheTimestampsModel {
private readonly _cacheName: string;
private _db: IDBPDatabase | null = null;
/**
*
* @param {string} cacheName
*
* @private
*/
constructor(cacheName: string) {
this._cacheName = cacheName;
}
/**
* Performs an upgrade of indexedDB.
*
* @param {IDBPDatabase} db
*
* @private
*/
private _upgradeDb(db: IDBPDatabase) {
// TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
// have to use the `id` keyPath here and create our own values (a
// concatenation of `url + cacheName`) instead of simply using
// `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
const objStore = db.createObjectStore(CACHE_OBJECT_STORE, {keyPath: 'id'});
// TODO(philipwalton): once we don't have to support EdgeHTML, we can
// create a single index with the keyPath `['cacheName', 'timestamp']`
// instead of doing both these indexes.
objStore.createIndex('cacheName', 'cacheName', {unique: false});
objStore.createIndex('timestamp', 'timestamp', {unique: false});
}
/**
* Performs an upgrade of indexedDB and deletes deprecated DBs.
*
* @param {IDBPDatabase} db
*
* @private
*/
private _upgradeDbAndDeleteOldDbs(db: IDBPDatabase) {
this._upgradeDb(db);
if (this._cacheName) {
void deleteDB(this._cacheName);
}
}
/**
* @param {string} url
* @param {number} timestamp
*
* @private
*/
async setTimestamp(url: string, timestamp: number): Promise {
url = normalizeURL(url);
const entry: CacheTimestampsModelEntry = {
url,
timestamp,
cacheName: this._cacheName,
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
id: this._getId(url),
};
const db = await this.getDb();
const tx = db.transaction(CACHE_OBJECT_STORE, 'readwrite', {
durability: 'relaxed',
});
await tx.store.put(entry);
await tx.done;
}
/**
* Returns the timestamp stored for a given URL.
*
* @param {string} url
* @return {number | undefined}
*
* @private
*/
async getTimestamp(url: string): Promise {
const db = await this.getDb();
const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url));
return entry?.timestamp;
}
/**
* Iterates through all the entries in the object store (from newest to
* oldest) and removes entries once either `maxCount` is reached or the
* entry's timestamp is less than `minTimestamp`.
*
* @param {number} minTimestamp
* @param {number} maxCount
* @return {Array}
*
* @private
*/
async expireEntries(
minTimestamp: number,
maxCount?: number,
): Promise {
const db = await this.getDb();
let cursor = await db
.transaction(CACHE_OBJECT_STORE)
.store.index('timestamp')
.openCursor(null, 'prev');
const entriesToDelete: CacheTimestampsModelEntry[] = [];
let entriesNotDeletedCount = 0;
while (cursor) {
const result = cursor.value;
// TODO(philipwalton): once we can use a multi-key index, we
// won't have to check `cacheName` here.
if (result.cacheName === this._cacheName) {
// Delete an entry if it's older than the max age or
// if we already have the max number allowed.
if (
(minTimestamp && result.timestamp < minTimestamp) ||
(maxCount && entriesNotDeletedCount >= maxCount)
) {
// TODO(philipwalton): we should be able to delete the
// entry right here, but doing so causes an iteration
// bug in Safari stable (fixed in TP). Instead we can
// store the keys of the entries to delete, and then
// delete the separate transactions.
// https://github.com/GoogleChrome/workbox/issues/1978
// cursor.delete();
// We only need to return the URL, not the whole entry.
entriesToDelete.push(cursor.value);
} else {
entriesNotDeletedCount++;
}
}
cursor = await cursor.continue();
}
// TODO(philipwalton): once the Safari bug in the following issue is fixed,
// we should be able to remove this loop and do the entry deletion in the
// cursor loop above:
// https://github.com/GoogleChrome/workbox/issues/1978
const urlsDeleted: string[] = [];
for (const entry of entriesToDelete) {
await db.delete(CACHE_OBJECT_STORE, entry.id);
urlsDeleted.push(entry.url);
}
return urlsDeleted;
}
/**
* Takes a URL and returns an ID that will be unique in the object store.
*
* @param {string} url
* @return {string}
*
* @private
*/
private _getId(url: string): string {
// Creating an ID from the URL and cache name won't be necessary once
// Edge switches to Chromium and all browsers we support work with
// array keyPaths.
return this._cacheName + '|' + normalizeURL(url);
}
/**
* Returns an open connection to the database.
*
* @private
*/
private async getDb() {
if (!this._db) {
this._db = await openDB(DB_NAME, 1, {
upgrade: this._upgradeDbAndDeleteOldDbs.bind(this),
});
}
return this._db;
}
}
export {CacheTimestampsModel};
================================================
FILE: packages/workbox-expiration/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"references": [{"path": "../workbox-core/"}]
}
================================================
FILE: packages/workbox-google-analytics/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-google-analytics
================================================
FILE: packages/workbox-google-analytics/package.json
================================================
{
"name": "workbox-google-analytics",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "Queues failed requests and uses the Background Sync API to replay them when the network is available",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"offline",
"google",
"analytics"
],
"workbox": {
"browserNamespace": "workbox.googleAnalytics",
"outputFilename": "workbox-offline-ga",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"workbox-background-sync": "7.4.0",
"workbox-core": "7.4.0",
"workbox-routing": "7.4.0",
"workbox-strategies": "7.4.0"
}
}
================================================
FILE: packages/workbox-google-analytics/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:google-analytics:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-google-analytics/src/index.ts
================================================
/*
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 {initialize, GoogleAnalyticsInitializeOptions} from './initialize.js';
import './_version.js';
/**
* @module workbox-google-analytics
*/
export {initialize, GoogleAnalyticsInitializeOptions};
================================================
FILE: packages/workbox-google-analytics/src/initialize.ts
================================================
/*
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 {BackgroundSyncPlugin} from 'workbox-background-sync/BackgroundSyncPlugin.js';
import {Queue} from 'workbox-background-sync/Queue.js';
import {cacheNames} from 'workbox-core/_private/cacheNames.js';
import {getFriendlyURL} from 'workbox-core/_private/getFriendlyURL.js';
import {logger} from 'workbox-core/_private/logger.js';
import {RouteMatchCallbackOptions} from 'workbox-core/types.js';
import {Route} from 'workbox-routing/Route.js';
import {Router} from 'workbox-routing/Router.js';
import {NetworkFirst} from 'workbox-strategies/NetworkFirst.js';
import {NetworkOnly} from 'workbox-strategies/NetworkOnly.js';
import {
QUEUE_NAME,
MAX_RETENTION_TIME,
GOOGLE_ANALYTICS_HOST,
GTM_HOST,
ANALYTICS_JS_PATH,
GTAG_JS_PATH,
GTM_JS_PATH,
COLLECT_PATHS_REGEX,
} from './utils/constants.js';
import './_version.js';
export interface GoogleAnalyticsInitializeOptions {
cacheName?: string;
parameterOverrides?: {[paramName: string]: string};
hitFilter?: (params: URLSearchParams) => void;
}
/**
* Creates the requestWillDequeue callback to be used with the background
* sync plugin. The callback takes the failed request and adds the
* `qt` param based on the current time, as well as applies any other
* user-defined hit modifications.
*
* @param {Object} config See {@link workbox-google-analytics.initialize}.
* @return {Function} The requestWillDequeue callback function.
*
* @private
*/
const createOnSyncCallback = (config: GoogleAnalyticsInitializeOptions) => {
return async ({queue}: {queue: Queue}) => {
let entry;
while ((entry = await queue.shiftRequest())) {
const {request, timestamp} = entry;
const url = new URL(request.url);
try {
// Measurement protocol requests can set their payload parameters in
// either the URL query string (for GET requests) or the POST body.
const params =
request.method === 'POST'
? new URLSearchParams(await request.clone().text())
: url.searchParams;
// Calculate the qt param, accounting for the fact that an existing
// qt param may be present and should be updated rather than replaced.
const originalHitTime = timestamp! - (Number(params.get('qt')) || 0);
const queueTime = Date.now() - originalHitTime;
// Set the qt param prior to applying hitFilter or parameterOverrides.
params.set('qt', String(queueTime));
// Apply `parameterOverrides`, if set.
if (config.parameterOverrides) {
for (const param of Object.keys(config.parameterOverrides)) {
const value = config.parameterOverrides[param];
params.set(param, value);
}
}
// Apply `hitFilter`, if set.
if (typeof config.hitFilter === 'function') {
config.hitFilter.call(null, params);
}
// Retry the fetch. Ignore URL search params from the URL as they're
// now in the post body.
await fetch(
new Request(url.origin + url.pathname, {
body: params.toString(),
method: 'POST',
mode: 'cors',
credentials: 'omit',
headers: {'Content-Type': 'text/plain'},
}),
);
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Request for '${getFriendlyURL(url.href)}' ` + `has been replayed`,
);
}
} catch (err) {
await queue.unshiftRequest(entry);
if (process.env.NODE_ENV !== 'production') {
logger.log(
`Request for '${getFriendlyURL(url.href)}' ` +
`failed to replay, putting it back in the queue.`,
);
}
throw err;
}
}
if (process.env.NODE_ENV !== 'production') {
logger.log(
`All Google Analytics request successfully replayed; ` +
`the queue is now empty!`,
);
}
};
};
/**
* Creates GET and POST routes to catch failed Measurement Protocol hits.
*
* @param {BackgroundSyncPlugin} bgSyncPlugin
* @return {Array} The created routes.
*
* @private
*/
const createCollectRoutes = (bgSyncPlugin: BackgroundSyncPlugin) => {
const match = ({url}: RouteMatchCallbackOptions) =>
url.hostname === GOOGLE_ANALYTICS_HOST &&
COLLECT_PATHS_REGEX.test(url.pathname);
const handler = new NetworkOnly({
plugins: [bgSyncPlugin],
});
return [new Route(match, handler, 'GET'), new Route(match, handler, 'POST')];
};
/**
* Creates a route with a network first strategy for the analytics.js script.
*
* @param {string} cacheName
* @return {Route} The created route.
*
* @private
*/
const createAnalyticsJsRoute = (cacheName: string) => {
const match = ({url}: RouteMatchCallbackOptions) =>
url.hostname === GOOGLE_ANALYTICS_HOST &&
url.pathname === ANALYTICS_JS_PATH;
const handler = new NetworkFirst({cacheName});
return new Route(match, handler, 'GET');
};
/**
* Creates a route with a network first strategy for the gtag.js script.
*
* @param {string} cacheName
* @return {Route} The created route.
*
* @private
*/
const createGtagJsRoute = (cacheName: string) => {
const match = ({url}: RouteMatchCallbackOptions) =>
url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
const handler = new NetworkFirst({cacheName});
return new Route(match, handler, 'GET');
};
/**
* Creates a route with a network first strategy for the gtm.js script.
*
* @param {string} cacheName
* @return {Route} The created route.
*
* @private
*/
const createGtmJsRoute = (cacheName: string) => {
const match = ({url}: RouteMatchCallbackOptions) =>
url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
const handler = new NetworkFirst({cacheName});
return new Route(match, handler, 'GET');
};
/**
* @param {Object=} [options]
* @param {Object} [options.cacheName] The cache name to store and retrieve
* analytics.js. Defaults to the cache names provided by `workbox-core`.
* @param {Object} [options.parameterOverrides]
* [Measurement Protocol parameters](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters),
* expressed as key/value pairs, to be added to replayed Google Analytics
* requests. This can be used to, e.g., set a custom dimension indicating
* that the request was replayed.
* @param {Function} [options.hitFilter] A function that allows you to modify
* the hit parameters prior to replaying
* the hit. The function is invoked with the original hit's URLSearchParams
* object as its only argument.
*
* @memberof workbox-google-analytics
*/
const initialize = (options: GoogleAnalyticsInitializeOptions = {}): void => {
const cacheName = cacheNames.getGoogleAnalyticsName(options.cacheName);
const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
maxRetentionTime: MAX_RETENTION_TIME,
onSync: createOnSyncCallback(options),
});
const routes = [
createGtmJsRoute(cacheName),
createAnalyticsJsRoute(cacheName),
createGtagJsRoute(cacheName),
...createCollectRoutes(bgSyncPlugin),
];
const router = new Router();
for (const route of routes) {
router.registerRoute(route);
}
router.addFetchListener();
};
export {initialize};
================================================
FILE: packages/workbox-google-analytics/src/utils/constants.ts
================================================
/*
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 '../_version.js';
export const QUEUE_NAME = 'workbox-google-analytics';
export const MAX_RETENTION_TIME = 60 * 48; // Two days in minutes
export const GOOGLE_ANALYTICS_HOST = 'www.google-analytics.com';
export const GTM_HOST = 'www.googletagmanager.com';
export const ANALYTICS_JS_PATH = '/analytics.js';
export const GTAG_JS_PATH = '/gtag/js';
export const GTM_JS_PATH = '/gtm.js';
export const COLLECT_DEFAULT_PATH = '/collect';
// This RegExp matches all known Measurement Protocol single-hit collect
// endpoints. Most of the time the default path (/collect) is used, but
// occasionally an experimental endpoint is used when testing new features,
// (e.g. /r/collect or /j/collect)
export const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/;
================================================
FILE: packages/workbox-google-analytics/tsconfig.json
================================================
{
"extends": "../../tsconfig",
"compilerOptions": {
"composite": true,
"outDir": "./",
"rootDir": "./src",
"tsBuildInfoFile": "./tsconfig.tsbuildinfo"
},
"include": ["src/**/*.ts"],
"references": [
{"path": "../workbox-background-sync/"},
{"path": "../workbox-core/"},
{"path": "../workbox-routing/"},
{"path": "../workbox-strategies/"}
]
}
================================================
FILE: packages/workbox-navigation-preload/README.md
================================================
This module's documentation can be found at https://developers.google.com/web/tools/workbox/modules/workbox-navigation-preload
================================================
FILE: packages/workbox-navigation-preload/package.json
================================================
{
"name": "workbox-navigation-preload",
"version": "7.4.0",
"license": "MIT",
"author": "Google's Web DevRel Team and Google's Aurora Team",
"description": "This library allows developers to opt-in to using Navigation Preload in their service worker.",
"repository": {
"type": "git",
"url": "git+https://github.com/googlechrome/workbox.git"
},
"bugs": "https://github.com/googlechrome/workbox/issues",
"homepage": "https://github.com/GoogleChrome/workbox",
"keywords": [
"workbox",
"workboxjs",
"service worker",
"sw",
"navigation"
],
"workbox": {
"browserNamespace": "workbox.navigationPreload",
"packageType": "sw"
},
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"dependencies": {
"workbox-core": "7.4.0"
}
}
================================================
FILE: packages/workbox-navigation-preload/src/_version.ts
================================================
// @ts-ignore
try{self['workbox:navigation-preload:7.4.0']&&_()}catch(e){}
================================================
FILE: packages/workbox-navigation-preload/src/disable.ts
================================================
/*
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 {logger} from 'workbox-core/_private/logger.js';
import {isSupported} from './isSupported.js';
import './_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
/**
* If the browser supports Navigation Preload, then this will disable it.
*
* @memberof workbox-navigation-preload
*/
function disable(): void {
if (isSupported()) {
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
self.registration.navigationPreload.disable().then(() => {
if (process.env.NODE_ENV !== 'production') {
logger.log(`Navigation preload is disabled.`);
}
}),
);
});
} else {
if (process.env.NODE_ENV !== 'production') {
logger.log(`Navigation preload is not supported in this browser.`);
}
}
}
export {disable};
================================================
FILE: packages/workbox-navigation-preload/src/enable.ts
================================================
/*
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 {logger} from 'workbox-core/_private/logger.js';
import {isSupported} from './isSupported.js';
import './_version.js';
// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope;
/**
* If the browser supports Navigation Preload, then this will enable it.
*
* @param {string} [headerValue] Optionally, allows developers to
* [override](https://developers.google.com/web/updates/2017/02/navigation-preload#changing_the_header)
* the value of the `Service-Worker-Navigation-Preload` header which will be
* sent to the server when making the navigation request.
*
* @memberof workbox-navigation-preload
*/
function enable(headerValue?: string): void {
if (isSupported()) {
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
self.registration.navigationPreload.enable().then(() => {
// Defaults to Service-Worker-Navigation-Preload: true if not set.
if (headerValue) {
void self.registration.navigationPreload.setHeaderValue(
headerValue,
);
}
if (process.env.NODE_ENV !== 'production') {
logger.log(`Navigation preload is enabled.`);
}
}),
);
});
} else {
if (process.env.NODE_ENV !== 'production') {
logger.log(`Navigation preload is not supported in this browser.`);
}
}
}
export {enable};
================================================
FILE: packages/workbox-navigation-preload/src/index.ts
================================================
/*
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 {disable} from './disable.js';
import {enable} from './enable.js';
import {isSupported} from './isSupported.js';
import './_version.js';
// See https://github.com/GoogleChrome/workbox/issues/2946
interface NavigationPreloadState {
enabled?: boolean;
headerValue?: string;
}
interface NavigationPreloadManager {
disable(): Promise;
enable(): Promise