vite-pwa / vite-plugin-pwa

Zero-config PWA for Vite
https://vite-pwa-org.netlify.app/
MIT License
3.07k stars 201 forks source link

Resolve imports when bundling #35

Closed kolaente closed 3 years ago

kolaente commented 3 years ago

As per the workbox docs, I have this in my service worker file:

import { precacheAndRoute } from 'workbox-precaching'
precacheAndRoute(self.__WB_MANIFEST)

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 error Uncaught 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:

VitePWA({
    strategies: 'injectManifest',
    injectRegister: false, // service worker is registered from a separate script.
    injectManifest: {
        swSrc: './src/ServiceWorker/sw.js',
        swDest: './dist/sw.js',
    },
    manifest: {
        // manifest omitted
    }
}),
kolaente commented 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.

userquin commented 3 years ago

yes, I remove the comment, you have to deal with workbox, the error is on workbox side not in VitePWA...

userquin commented 3 years ago

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
userquin commented 3 years ago

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

userquin commented 3 years ago

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.

kolaente commented 3 years ago

@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.

userquin commented 3 years ago

@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.

userquin commented 3 years ago

@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"},
...
...
];
...
...
userquin commented 3 years ago

@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...

userquin commented 3 years ago

@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*/{...})
Mehdi-Hp commented 3 years ago

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 "
}
kolaente commented 3 years ago

@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.

kolaente commented 3 years ago

@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 commented 3 years ago

@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 ;).

hannoeru commented 3 years ago

If someone wants to finish bundle-sw branch, feel free to do that!

userquin commented 3 years ago

@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.

userquin commented 3 years ago

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.

imagen

userquin commented 3 years ago

imagen