Closed kolaente closed 3 years ago
That gives me this error when building:
(node:18630) UnhandledPromiseRejectionWarning: Error: The 'swSrc' file can't be read. ENOENT: no such file or directory, open './public/sw.js'
at injectManifest (node_modules/workbox-build/build/inject-manifest.js:152:11)
(node:18630) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:18630) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
The error message makes kind of sense since my service worker file is not in public but rather in a sub directory of src
. Hence my custom swSrc
configuration.
The docs say I'd need to configure that top level when using injectManifest
.
yes, I remove the comment, you have to deal with workbox, the error is on workbox side not in VitePWA...
Hi again, I have it working, I ask @antfu and waiting its response:
// see below: just change imports by assignments
// import { cacheNames } from 'workbox-core'
// import { registerRoute, setCatchHandler, setDefaultHandler } from 'workbox-routing'
// import { CacheableResponsePlugin } from 'workbox-cacheable-response'
// import {
// NetworkFirst,
// StaleWhileRevalidate,
// NetworkOnly,
// } from 'workbox-strategies'
// import { ExpirationPlugin } from 'workbox-expiration'
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.1.1/workbox-sw.js')
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: true,
})
// To avoid async issues, we load strategies before we call it in the event listener
workbox.loadModule('workbox-core')
workbox.loadModule('workbox-routing')
workbox.loadModule('workbox-cacheable-response')
workbox.loadModule('workbox-strategies')
workbox.loadModule('workbox-expiration')
const cacheNames = workbox.core.cacheNames
const { registerRoute, setCatchHandler, setDefaultHandler } = workbox.routing
const { CacheableResponsePlugin } = workbox.cacheableResponse
const {
NetworkFirst,
StaleWhileRevalidate,
NetworkOnly,
} = workbox.strategies
const { ExpirationPlugin } = workbox.expiration
const cacheName = cacheNames.runtime
The problem is that the sw.js is sent to the client, but this script cannot be a module, you need to transform it or use the approach using importScripts
instead import
.
https://developers.google.com/web/tools/workbox/modules/workbox-sw
I have found this bug: https://bugs.chromium.org/p/chromium/issues/detail?id=824647
We cannot use esmodules yet on service workers: once implemented, we can use it using type: 'module'
on RegistrationOptions
:
if('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/', type: 'module' })
})
}
The broswer fails to load the sw with:
type 'module' in RegistrationOptions is not implemented yet.See https://crbug.com/824647 for details.
@userquin how would I go about transforming the SW? I actually thought this is what this plugin would do.
I knew about the module type when registering a service worker, it's just not something I would do for a production app given the lack of browser support at the moment.
@kolaente Yeah, I was talking with @antfu and there is a pending branch here (bundle-sw).
Anyway, in the docs, it is marked as WIP: working in progress
, so you cannot configure the plugin in this way (the plugin, right now, just will paste the source in the the destination, if it is a module it will not work, so you need to change your sw.js
to use importScripts
).
You need to transform it manually or programatically. I'm working on NetworkFirst
for docsites
: the code in my previous comment is just a snippet from my local tests for this new strategy.
You can use getManifest from workbox to generate the manifest (self.__WB_MANIFEST
equivalent) and then configure a plugin to vite to build your sw.js
using getManifest
. For the Vite Plugin
, you can see an example here, just follow the imports.
@kolaente workbox
folks haven't consider this yet, just see issue 2588 from workbox
repo, maybe on v7
... or once resolved https://bugs.chromium.org/p/chromium/issues/detail?id=824647 (and in all browsers)
You can see there some implementations, for example see this using workbox-v4 and look into package.json:
I use second approach:
1) extract workbox libraries in build directory via copyWorkboxLibraries
from workbox-util
: this will give me the version of workbox
to be used in next step: right now the version is workbox-v6.1.1
2) replace on internal template the path for importPaths
and configure workbox
:
const swSrc = resolve(__dirname, options.swDest)
// extract workbox libraries
const workboxDirectoryName = await copyWorkboxLibraries(resolve(options.outDir))
// read template
const content = await fs.readFile(resolve(__dirname, 'client/build/networkfirst.mjs'), 'utf-8')
// configure workbox template
const result = content
.replace('__SW_WS__', `${options.base}workbox-sw/index.mjs`)
.replace('__SW_IMPORT_SCRIPTS__', `${slash(join(options.base, workboxDirectoryName, '/workbox-sw.js'))}`)
.replace('__SW_MODULE_PATH_PREFIX__', `${slash(join(options.base, workboxDirectoryName, '/'))}`)
.replace('__SW_DEBUG__', options.mode === 'production' ? 'false' : 'true')
.replace('__SW_INDEX_HTML__', index)
await fs.writeFile(swSrc, result, {
encoding: 'utf-8',
})
3) save the preconfigured template (const result =
from previous step) in the destination directory: this will be the sw.js
file.
4) call injectManifest
from workbox-util
using:
const swSrc = resolve(__dirname, options.swDest)
// inject manifest
await injectManifest({
swSrc,
swDest: swSrc, // this will not fail since there is an injectionPoint
injectionPoint: 'self.__WB_MANIFEST',
globDirectory,
additionalManifestEntries,
dontCacheBustURLsMatching,
globFollow,
globIgnores,
globPatterns,
globStrict,
manifestTransforms,
maximumFileSizeToCacheInBytes,
modifyURLPrefix,
templatedURLs,
})
Here a snippet of my service-worker
template:
networkfirst.ts
/* eslint-disable comma-dangle */
// @ts-ignore
importScripts('__SW_IMPORT_SCRIPTS__')
// @ts-ignore
declare let self: ServiceWorkerGlobalScope
declare let workbox: any
// Note: Ignore the error that Glitch raises about workbox being undefined.
workbox.setConfig({
debug: JSON.parse('__SW_DEBUG__') === true,
modulePathPrefix: '__SW_MODULE_PATH_PREFIX__',
})
// To avoid async issues, we load strategies before we call it in the event listener
workbox.loadModule('workbox-core')
workbox.loadModule('workbox-routing')
workbox.loadModule('workbox-cacheable-response')
workbox.loadModule('workbox-strategies')
workbox.loadModule('workbox-expiration')
const cacheNames = workbox.core.cacheNames
const { registerRoute, setCatchHandler, setDefaultHandler } = workbox.routing
const { CacheableResponsePlugin } = workbox.cacheableResponse
const {
NetworkFirst,
StaleWhileRevalidate,
NetworkOnly
} = workbox.strategies
const { ExpirationPlugin } = workbox.expiration
// @ts-ignore
const manifest = self.__WB_MANIFEST
...
...
with this result:
// src/client/build/networkfirst.ts
importScripts("/workbox-v6.1.1/workbox-sw.js");
workbox.setConfig({
debug: JSON.parse("true") === true,
modulePathPrefix: "/workbox-v6.1.1/"
});
workbox.loadModule("workbox-core");
workbox.loadModule("workbox-routing");
workbox.loadModule("workbox-cacheable-response");
workbox.loadModule("workbox-strategies");
workbox.loadModule("workbox-expiration");
var cacheNames = workbox.core.cacheNames;
var {registerRoute, setCatchHandler, setDefaultHandler} = workbox.routing;
var {CacheableResponsePlugin} = workbox.cacheableResponse;
var {
NetworkFirst,
StaleWhileRevalidate,
NetworkOnly
} = workbox.strategies;
var {ExpirationPlugin} = workbox.expiration;
var manifest = [
{"revision":"fa8820bba32afa2ec310297ff3c52601","url":"assets/[name].0718b5f8.js"},
{"revision":"dd6d1382ece531785b016772a7ab1f6a","url":"assets/about.c4a91220.js"},
{"revision":"1b13e9d07bf3590700bb2dfe4f8adc8d","url":"assets/home.379f6d36.js"},
{"revision":"5145da3e9a7f1e16f62028ba40335f70","url":"assets/index.7685bec4.js"},
{"revision":"a760355bef2fd5ded385ccabf84c8de0","url":"assets/index.f3bb671a.css"},
{"revision":"440f26a0e8951f5454d0dae54bee0822","url":"index.html"},
{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},
...
...
];
...
...
@hannoeru maybe this will help you for bundle-sw
. I have a pending push on sw-enhancement
branch to include Network first
for injectRegister
.
If you need some feedback, just contact me here or on discord...
@hannoeru I have pushed sw-networkfirst
branch
I'm trying another approach: see https://developers.google.com/web/tools/workbox/modules/workbox-recipes
Instead having an awful ts
file with importScript
I'm doing some play with the link above, trying to do the same but for custom receipts.
The result will be more redeable (see src/receipts/networkfirst/index.ts
):
import { networkfirst } from 'vite-plugin-pwa/receipts/networkfirst'
networkfirst(self.__WB_MANIFEST, /*options*/{...})
Until a more robust solution, I went with bundling service-worker.js after Vite's build.
rollup.sw.js
import { nodeResolve } from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import replace from '@rollup/plugin-replace';
export default {
input: './dist/sw.js',
output: {
dir: 'dist',
format: 'esm'
},
plugins: [
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
'preventAssignment': true
}),
nodeResolve({
browser: true
}),
commonjs(),
babel({
exclude: '**/node_modules/**',
extensions: ['js'],
babelHelpers: 'runtime',
presets: [
[
'@babel/preset-env',
{
corejs: 3,
useBuiltIns: 'entry',
targets: {
'esmodules': true
},
modules: false
}
]
]
}),
terser()
]
};
package.json
{
"build": "vite build && rollup -c ./rollup.sw.js "
}
@userquin The bundle branch looks like a great solution. When do you think this could be used in production? I'd happily beta test it.
@Mehdi-Hp looks great per se, wondering if that could be integrated more into vite. But I guess then we're back to this plugin again 🙂
@userquin The bundle branch looks like a great solution. When do you think this could be used in production? I'd happily beta test it.
@hannoeru or @antfu should answer this: when done I will use it on the recipes / receipts
ones,sw-networkfisrt
is a proof of concept I am toying with.
The idea behind this is to abstract the user from using workbox
, rollup
, babel
, vite
... Just use what this plugin provides, it is very ambitious ;).
If someone wants to finish bundle-sw branch, feel free to do that!
@antfu @hannoeru
I'm playing with web workers and it is really strange: I thought that we cannot use new Worker('someurl', { type: 'module' })
on safari and firefox, but this is wrong. We can use module
in all browsers, but in firefox and safari, the content cannot have import
statements (an es module), this is not allowed (just plain javascript, es2017 or whatever we want, or using importScripts
).
I have a web worker with 128 modules with type script, and testing on a separate project (copy/paste from the original to isolate the logic), using esno
and tsup
, I can generate both, cjs
and esm
builds. When generating esm
version I just, move my projects dependencies
to dev dependencies
and done: my web webworker module
is built with all dependencies inside.
I have both versions working with and without {type: 'module'}
when creating the Worker
(for esm
variant and cjs
respectivelly).
On service workers, right now, we cannot use 'import' statements inside the service worker (in any browser), so we can use the same approach I use for web workers, that is, do not use importScripts
and try to make a bundle with all dependencies inside it (we just need to find a way to do this). Here, code splitting doesn't matter, the service worker will ship all necessary dependencies inside it, workbox.*
packages.
I'll try to do a simple test...
about web worker, I have this:
src/parse-file-worker-module.ts
import { expose } from 'comlink'
import { parseFileAsync } from '~/parse-file-async' // <=== 128 typscript modules with imports
const parseFile = async(
file,
onFileInfo,
onControlParsed,
onProgress,
onProcessError,
onFinished,
canceled,
) => {
// @ts-ignore
await parseFileAsync(
file,
onFileInfo,
onControlParsed,
onProgress,
onProcessError,
onFinished,
canceled,
)
}
// SSR compatibility
// @ts-ignore
typeof self !== 'undefined' && expose(parseFile)
with service worker, its corresponding code will be:
import { precacheAndRoute } from 'workbox-precaching'
precacheAndRoute(self.__WB_MANIFEST)
then using this script and running it:
package.json
"scripts": {
"build": "esno scripts/build.ts"
}
scripts/build.ts
npx tsup src/parse-file-worker-module.ts --sourcemap --format esm -d dist/build
the result is is parse-file-worker-module.mjs
with all dependencies added instead import statements
that works using it with:
const worker = new Worker('/parse-file-worker-module.mjs', { type: 'module' })
If we can do this with the sw.js
problem solved, but first we need to test it works.
The test is working, with some manual configuration but at least working, now we need to find a way to do the following, if possible:
First, the configuration:
1) add tsup
, esno
and workbox-precaching
as dev dependencies
for the target project: in my case, I use examples/vue-basic
from sw-enhancement
branch:
cd examples/vue-basic
pnpm i --save-dev esno tsup workbox-precaching
2) add sw.ts
to examples/vue-basic
with content:
import { precacheAndRoute } from 'workbox-precaching'
console.log('QUE PASA TRON')
// self.__WB_MANIFEST is default injection point
// @ts-ignore
precacheAndRoute(self.__WB_MANIFEST)
3) add scripts/build-sw.ts
on examples/vue-basic
:
import { execSync } from 'child_process'
execSync('npx tsup sw.ts --no-splitting --format cjs -d .', { stdio: 'inherit' })
4) add the following scripts to package.json
(on root):
"example:build:sw": "npm -C examples/vue-basic run build-sw",
"example:start:sw": "npm -C examples/vue-basic run start-sw",
5) add the following scripts to examples/vue-basic/package.json`:
"start-sw": "npm run run-build-sw && npm run serve",
"run-build-sw": "cross-env DEBUG=vite-plugin-pwa:* BASE_URL=/ SOURCE_MAP=true SW=true vite build",
"build-sw": "esno scripts/build-sw.ts",
6) modify examples/vue-basic/vite.config.ts
with:
import { UserConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import { VitePWA, Options as VitePWAOptions } from 'vite-plugin-pwa'
import replace from '@rollup/plugin-replace'
const pwaConfig: Partial<VitePWAOptions> = {
mode: 'development',
base: '/',
}
if (process.env.SW === 'true') {
pwaConfig.strategies = 'injectManifest'
pwaConfig.injectManifest = {
swSrc: 'sw.js',
}
}
const config: UserConfig = {
// base: process.env.BASE_URL || 'https://github.com/',
build: {
sourcemap: process.env.SOURCE_MAP === 'true',
},
plugins: [
Vue(),
VitePWA(pwaConfig),
replace({
__DATE__: new Date().toISOString(),
}),
],
}
export default config
Now build time:
1) run example:build-sw
script from root directory: pnpm run example:build-sw
2) edit generated sw.js
file on examples/vue-basic
directory, adding:
const process = {
env: {
NODE_ENV: 'production',
},
}
below:
// ../../node_modules/.pnpm/workbox-core@6.1.2/node_modules/workbox-core/_version.js
"use strict";
try {
self["workbox:core:6.1.2"] && _();
} catch (e) {
}
3) run example:start:sw
script from root directory: pnpm run example:start:sw
The server will start and you can test it.
As per the workbox docs, I have this in my service worker file:
However, when I build my app, that part stays the same which results in the service worker not working because browsers can't execute js
import
statements in service workers (at least for now). It fails to execute the service worker with the errorUncaught SyntaxError: Cannot use import statement outside a module
.Not sure if this is an issue with the bundler or my understanding of workbox. All examples use import statements.
Relevant part of my
vite.config.js
: