Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: serviceworker #7163

Closed
4 tasks done
mathe42 opened this issue Mar 3, 2022 · 10 comments
Closed
4 tasks done

feat: serviceworker #7163

mathe42 opened this issue Mar 3, 2022 · 10 comments

Comments

@mathe42
Copy link
Contributor

mathe42 commented Mar 3, 2022

Clear and concise description of the problem

With vite 2.8 we got better worker support with a custom build pipeline.

I propose the same for serviceworker so the following should be transformed:

navigator.serviceWorker.register(new URL('./sw.js', import.meta.url), {/**/})

Suggested solution

This could be a copy of the current worker implementation with the difference that service worker can't be modules so it allways has to get compiled to an iife. This is also true while in development.

So options should be something like:

export declare interface UserConfig {
   //...
    /**
     * ServiceWorker bundle options
     */
     serviceWorker?: {
        /**
         * Vite plugins that apply to serviceWorker bundle
         */
        plugins?: (PluginOption | PluginOption[])[];
        /**
         * Rollup options to build worker bundle
         */
        rollupOptions?: Omit<RollupOptions, 'plugins' | 'input' | 'onwarn' | 'preserveEntrySignatures'>;
        /**
        * Filename of ServiceWorker in bundle as this should NEVER change
        * @default sw.js
        */
        filename?: string
    };
}

This has some open questions:

  1. What happens when using multiple serviceWorker (with different scope) [how determan filename]

Alternative

No response

Additional context

The same might be usable for Worklets. (Audio, Paint, Animation, Layout)

Validations

@sapphi-red
Copy link
Member

related: #2248

@mathe42
Copy link
Contributor Author

mathe42 commented Apr 21, 2022

I would love to create a PR for that if the proposed options additions are approved by a maintainer!

@userquin
Copy link
Contributor

@mathe42 you can check vite plugin pwa, it supports also development mode, check docs here https://vite-plugin-pwa.netlify.app/guide/development.html

@mathe42
Copy link
Contributor Author

mathe42 commented Apr 22, 2022

I want full control over the SW so not what I need....

@rockwotj
Copy link
Contributor

This could be a copy of the current worker implementation with the difference that service worker can't be modules so it allways has to get compiled to an iife. This is also true while in development.

That isn't true, it can be modules if you use the right option.

https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register

@mathe42
Copy link
Contributor Author

mathe42 commented Jun 20, 2022

Yes but as far as I know No Browser actualy Supports this option.

@rockwotj
Copy link
Contributor

rockwotj commented Jun 20, 2022

It seems to work in Chrome for me just fine

Anyways, here's a small plugin mostly copied from the existing worker plugin built into vite that we're using to do this:

import {createHash} from 'crypto';
import path from 'path';

import isNil from 'lodash/isNil';
import type Rollup from 'rollup';
import {Plugin, ResolvedConfig} from 'vite';

const requestQuerySplitRE = /\?(?!.*[/|}])/;
function parseRequest(id: string): Record<string, string> | null {
  const [, search] = id.split(requestQuerySplitRE, 2);
  return search ? Object.fromEntries(new URLSearchParams(search)) : null;
}

const queryRE = /\?.*$/s;
const hashRE = /#.*$/s;
function cleanUrl(url: string): string {
  return url.replace(hashRE, '').replace(queryRE, '');
}

function getAssetHash(content: Buffer): string {
  return createHash('sha256').update(content).digest('hex').slice(0, 8);
}

function emitSourcemapForWorkerEntry(
  ctx: Rollup.TransformPluginContext,
  config: ResolvedConfig,
  id: string,
  chunk: Rollup.OutputChunk
): Buffer {
  if (chunk.map) {
    const basename = path.parse(cleanUrl(id)).name;
    const data = chunk.map.toString();
    const content = Buffer.from(data);
    const contentHash = getAssetHash(content);
    const fileName = `${basename}.${contentHash}.js.map`;
    const filePath = path.posix.join(config.build.assetsDir, fileName);
    ctx.emitFile({
      fileName: filePath,
      type: 'asset',
      source: data,
    });
    chunk.code += `//# sourceMappingURL=${fileName}`;
  }
  return Buffer.from(chunk.code);
}

export async function bundleWorkerEntry(
  ctx: Rollup.TransformPluginContext,
  config: ResolvedConfig,
  id: string
): Promise<Buffer> {
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const rollup = require('rollup') as typeof Rollup;
  const {plugins, rollupOptions, format} = config.worker;
  const bundle = await rollup.rollup({
    ...rollupOptions,
    input: cleanUrl(id),
    plugins,
    preserveEntrySignatures: false,
  });
  let chunk: Rollup.OutputChunk;
  try {
    const {
      output: [outputChunk, ...outputChunks],
    } = await bundle.generate({
      format,
      sourcemap: config.build.sourcemap,
    });
    chunk = outputChunk;
    outputChunks.forEach((outputChunk) => {
      if (outputChunk.type === 'asset') {
        ctx.emitFile(outputChunk);
      } else if (outputChunk.type === 'chunk') {
        ctx.emitFile({
          fileName: path.posix.join(config.build.assetsDir, outputChunk.fileName),
          source: outputChunk.code,
          type: 'asset',
        });
      }
    });
  } finally {
    await bundle.close();
  }
  return emitSourcemapForWorkerEntry(ctx, config, id, chunk);
}

export async function workerFileToUrl(
  ctx: Rollup.TransformPluginContext,
  config: ResolvedConfig,
  id: string
): Promise<string> {
  const code = await bundleWorkerEntry(ctx, config, id);
  const basename = path.parse(cleanUrl(id)).name;
  const contentHash = getAssetHash(code);
  const fileName = path.posix.join(config.build.assetsDir, `${basename}.${contentHash}.js`);
  const hash = ctx.emitFile({
    fileName,
    type: 'asset',
    source: code,
  });
  return `__VITE_ASSET__${hash}__`;
}

export function serviceWorkerPlugin(): Plugin {
  let resolved: ResolvedConfig;

  return {
    name: 'serviceworker',
    configResolved(config) {
      resolved = config;
    },
    load(id) {
      const parsedQuery = parseRequest(id);
      if (!parsedQuery || isNil(parsedQuery.serviceworker)) {
        return;
      }
      if (resolved.command === 'build') {
        return '';
      } else {
        return `export default ${JSON.stringify({
          url: cleanUrl(id),
          options: {type: 'module'},
        })};`;
      }
    },
    async transform(_, id) {
      if (resolved.command !== 'build') {
        return;
      }
      const parsedQuery = parseRequest(id);
      if (!parsedQuery || isNil(parsedQuery.serviceworker)) {
        return;
      }
      const url = await workerFileToUrl(this, resolved, id);
      return {
        code: `export default ${JSON.stringify({
          url: url,
          options: {type: resolved.worker.format === 'es' ? 'module' : 'classic'},
        })};`,
        map: {mappings: ''},
      };
    },
  };
}

@mathe42
Copy link
Contributor Author

mathe42 commented Jun 20, 2022

Thats great! I will have a Look into it but I currently have No Time to work on it.

If there is a vite-team member that approve that this is a Feature to add to Vite I would take some time to implement it and create a PR

@bluwy
Copy link
Member

bluwy commented Nov 9, 2023

Closing this in favour of #2248

@bluwy bluwy closed this as not planned Won't fix, can't repro, duplicate, stale Nov 9, 2023
@amw
Copy link

amw commented Nov 16, 2023

I can confirm that modules do work in ServiceWorkers today. Tested Chrome and Safari.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants