aklinker1 / vite-plugin-web-extension

Vite plugin for developing Chrome/Web Extensions
https://vite-plugin-web-extension.aklinker1.io/
MIT License
582 stars 50 forks source link

[FEATURE/QUESTION] HMR for content scripts #204

Closed CharlieDigital closed 1 month ago

CharlieDigital commented 1 month ago

Summary

From the docs: https://vite-plugin-web-extension.aklinker1.io/guide/development.html#development-mode

However, please note that content scripts do not support HMR. They are built using Watch Mode, see below. Thus, when you modify and save a file associated with a content script, it will trigger a full reload of the extension once the file has finished building.

If I understand this correctly, this plugin does not support HMR for client scripts in any mode (dev or watch).

This is also what I have observed based on experimentation.

Is your feature request related to a bug/issue?

I was playing around with another template: https://github.com/elwin013/vitaly-extension and noticed that HMR works fine.

https://github.com/user-attachments/assets/75aaaa07-3a91-47dd-bd91-2425b0d31968

As I am not too familiar with the internals of HMR and what Vite is doing, I was wondering if there is some incongruency with this plugin (vite-plugin-web-extension) that prevents it from working with HMR when it seems to be fairly straight forward (I did not notice anything special about the vitaly project.

Forgive my lack of understanding on this topic; just curious why HMR can't work for client scripts.

Alternatives

If I understand correctly, the code for HTML HMR is here: https://github.com/aklinker1/vite-plugin-web-extension/blob/main/packages/vite-plugin-web-extension/src/plugins/hmr-rewrite-plugin.ts#L87-L125

    async transform(code, id) {
      // Only transform HTML inputs
      if (!id.endsWith(".html") || !inputIds.includes(id)) return;

      const baseUrl = "http://localhost:5173";

      // Load scripts from dev server, this adds the /@vite/client script to the page
      const serverCode = await server.transformIndexHtml(id, code);

      const { document } = parseHTML(serverCode);
      const pointToDevServer = (querySelector: string, attr: string): void => {
        document.querySelectorAll(querySelector).forEach((element) => {
          const src = element.getAttribute(attr);
          if (!src) return;

          const before = element.outerHTML;

          if (path.isAbsolute(src)) {
            element.setAttribute(attr, baseUrl + src);
          } else if (src.startsWith(".")) {
            const abs = path.resolve(path.dirname(id), src);
            const pathname = path.relative(paths.rootDir, abs);
            element.setAttribute(attr, `${baseUrl}/${pathname}`);
          }

          const after = element.outerHTML;
          if (before !== after) {
            logger.verbose(
              "Transformed for dev mode: " + inspect({ before, after })
            );
          }
        });
      };

      pointToDevServer("script[type=module]", "src");
      pointToDevServer("link[rel=stylesheet]", "href");

      // Return new HTML
      return document.toString();
    }

Namely, pointToDevServer here is manually re-writing the script.src and link.href to point to a vite endpoint. Is there a workaround to achieve HMR for Vue/React without doing this?

Additional Context

aklinker1 commented 1 month ago

Quick overview of HMR and vite:

ESM is required for HMR to work. So first, a content script needs to be ran in an ESM environment before any form of HMR can be implemented. Vitality uses CRXJS to power it. CRXJS "adds" content script ESM support by using dynamic imports like this:

// content-scripts/main-loader.js
await import("./main.js");

Then the main.js is loaded as ESM, and it can import modules from vite's dev server. Vite then does a lot of things internally to add HMR to the ESM files you import from the dev server.

That's a short version of how CRXJS adds HMR to content scripts.


However, there are two main downsides to this approach that I think make not useful:

  1. Since dynamic imports are asynchronous, content scripts don't respect run_at.

    If you need to set run_at: "document_start", your script will be loaded asynchronously and will be executed... at some point. But not when document_start is supposed to run

  2. Shadow root styles during development

    Shadow roots are a very common way of isolating your content script UI's styles from the page's styles. The way vite manages CSS during development, you can't control where the <style> blocks are added. They always show up in the document <head>. If you're using shadow roots, that mean all your styles are ignored because you can't put them in the shadow root.

So the combination of these two things means the usefulness of ESM content scripts during development fall apart quickly, and they don't work during development.


vite-plugin-web-extension doesn't provide support for ESM content scripts, and I don't plan on adding support, so I'd recommend using CRXJS instead of none of the issues I mentioned above effect you, or will effect future features.

CharlieDigital commented 1 month ago

Appreciate the insight here and detailed explanation going through your decision making process and underlying code infrastructure; thanks for the effort!