vite-plugins icon indicating copy to clipboard operation
vite-plugins copied to clipboard

vite-plugin-external: Allow `externals` as function that is called on runtime

Open irshadahmad21 opened this issue 1 year ago • 6 comments

Hello,

Thank you for this wonderful package. It has been so helpful.

We have been using it to externalize some packages we develop in our monorepo and then externalize them. It works pretty well.

The only problem we have been facing is that we have to statically pass a key-value object to externals option and whenever there is a new package or a package gets removed/replaced, we need to update that externals list. We also externalize some third-party packages which belong to a namespace like @org-name and we have to regularly check that list upstream and ensure that we don't unintentionally bundle any newly created package from them.

So, sometimes we forget and it's only when we see bugs in production that we notice that list of external packages is outdated.

Thus, it would be great if we could use externals as a function which would work in the same way as rollupOptions.external and we could decide at runtime whether we want to externalize a package or not using some regex match - ^(@our-namespace|@third-party)\//.match(id)

Or another solution could be to fallback on rollupOptions.external, which we could then use to handle it dynamically.

irshadahmad21 avatar Jul 28 '24 10:07 irshadahmad21

@irshadahmad21 Can you provide a simple demo? I will use this demo to confirm after optimization

fengxinming avatar Aug 08 '24 10:08 fengxinming

Here is the simplest demo

https://stackblitz.com/edit/vite-external-demo?file=vite.config.ts

Instead of

createExternal({
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
})

We would want it to be something like this

createExternal((id: string) => {

	if (id.startsWith('@wordpress/')) {
		// "@wordpress/block-editor" => "wp.blockEditor"
		// "@wordpress/components" => "wp.components"
		const variable = dashToCamelCase(id.replace('@wordpress/', ''));

		return `wp.${variable}`;
	}
	// Other packages to externalize
	switch (id) {
		case 'react':
			return 'React';
		case 'react-dom':
			return 'ReactDOM';
	}

	// returning undefined means the package should not be externalized
});
function dashToCamelCase(input: string): string {
	return input.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}

rollup-plugin-external-globals uses the same technique.

irshadahmad21 avatar Aug 11 '24 12:08 irshadahmad21

Any update on this?

irshadahmad21 avatar Nov 24 '24 11:11 irshadahmad21

@irshadahmad21 Look at that https://github.com/fengxinming/vite-plugins/tree/dev/packages/vite-plugin-external#fix-rollup3188, you can use the new option externalGlobals. Sorry for the late reply

fengxinming avatar Feb 14 '25 15:02 fengxinming

After updating to the latest version, I tried this

viteExternalPlugin({
	externals: (id: string) => {
		if (id.startsWith('@wordpress/')) {
			// "@wordpress/block-editor" => "wp.blockEditor"
			// "@wordpress/components" => "wp.components"
			const variable = dashToCamelCase(id.replace('@wordpress/', ''));
	
			return `wp.${variable}`;
		}
		// Other packages to externalize
		switch (id) {
			case 'react':
				return 'React';
			case 'react-dom':
				return 'ReactDOM';
		}
	
		// returning undefined means the package should not be externalized
	},
	externalGlobals,
})

but it doesn't seem to work. That externals function never gets called on vite dev

irshadahmad21 avatar Feb 15 '25 04:02 irshadahmad21

Here is the demo https://stackblitz.com/edit/vite-external-demo-spgx9sye?file=vite.config.ts

  • Run npm run dev to see it doesn't err because the react CDN script is commented out in index.html
  • Run npm run build to see it bundles the react package

Notice the bundle size, which shouldn't be more than 3-4 kB Image

irshadahmad21 avatar Feb 15 '25 04:02 irshadahmad21

@irshadahmad21 Upgrade [email protected] and [email protected] to fix this issue. See https://stackblitz.com/edit/vite-external-demo-wuflfzcl?file=index.html

fengxinming avatar Mar 31 '25 02:03 fengxinming

Upgrade [email protected] and [email protected] to fix this issue. See https://stackblitz.com/edit/vite-external-demo-wuflfzcl?file=index.html

Thank you so much for working on the feature.

The issue is fixed for vite dev but not for vite build

If you run npm run build && npm run preview on your demo, you will see this error

  • Chrome:

    Uncaught TypeError: Failed to resolve module specifier "react".
    Relative references must start with either "/", "./", or "../".
    
  • Firefox:

    Uncaught TypeError: The specifier “react” was a bare specifier, but was not remapped to anything.
    Relative module specifiers must start with “./”, “../” or “/”.
    
Image

irshadahmad21 avatar Apr 01 '25 06:04 irshadahmad21

I think that other issue is related to #5.

irshadahmad21 avatar Apr 01 '25 06:04 irshadahmad21

@irshadahmad21 Usually we build bundles with iife format, like this

  build: {
    rollupOptions: {
      output: {
        format: 'iife',
      },
    },
  },

Because the default format is es, if you don't specify format in Vite config, you should add links in index.html, like this

<link
    rel="modulepreload"
    href="//cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js"
  />
  <link
    rel="modulepreload"
    href="//cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js"
  />

But react didn't provide es format bundles, then 'umd' files wouldn't be preload.

fengxinming avatar Apr 01 '25 14:04 fengxinming

@irshadahmad21 Just found a new way to solve this problem, see this demo https://stackblitz.com/edit/vite-external-demo-wuflfzcl?file=vite.config.ts .

Notice: 1、Define React variable like this import React, { useState } from 'react'; 2、Modify tsconfig.json like this "jsx": "preserve" 3、Set babel plugin option like thisjsxRuntime: 'classic' 4、Define imports in index.html like this

<script type="importmap">
  {
    "imports": {
      "react": "https://esm.sh/[email protected]",
      "react-dom/client": "https://esm.sh/[email protected]"
    }
  }
</script>
<link rel="modulepreload" href="https://esm.sh/[email protected]" />
<link rel="modulepreload" href="https://esm.sh/[email protected]" />

fengxinming avatar Apr 01 '25 15:04 fengxinming

Thank you for those suggestions.

I have been using this as a workaround and it works. I am not sure why that works and if we can use something similar in this library.

import rollupExternalGlobals from 'rollup-plugin-external-globals';

// Rest of the code

build: {
  rollupOptions: {
    plugins: [
      rollupExternalGlobals((libName) => {
        if (libName === 'react') {
          return 'React';
        }
        if (libName === 'react-dom/client') {
          return 'ReactDOM';
        }

        return '';
      }),
    ],
  },
},

Here is the demo: https://stackblitz.com/edit/vite-external-demo-gxrajv7d?file=vite.config.ts

  • Run npm run build && npm run preview

irshadahmad21 avatar Apr 02 '25 14:04 irshadahmad21

@irshadahmad21 Because 'rollup-plugin-external-globals' parses JS code again, I think it would be more slowly to build bundles. review that output bundle, you can see

(function polyfill() {
  const relList = document.createElement("link").relList;
  if (relList && relList.supports && relList.supports("modulepreload")) {
    return;
  }
  for (const link of document.querySelectorAll('link[rel="modulepreload"]')) {
    processPreload(link);
  }
  new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.type !== "childList") {
        continue;
      }
      for (const node of mutation.addedNodes) {
        if (node.tagName === "LINK" && node.rel === "modulepreload")
          processPreload(node);
      }
    }
  }).observe(document, { childList: true, subtree: true });
  function getFetchOpts(link) {
    const fetchOpts = {};
    if (link.integrity) fetchOpts.integrity = link.integrity;
    if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;
    if (link.crossOrigin === "use-credentials")
      fetchOpts.credentials = "include";
    else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";
    else fetchOpts.credentials = "same-origin";
    return fetchOpts;
  }
  function processPreload(link) {
    if (link.ep)
      return;
    link.ep = true;
    const fetchOpts = getFetchOpts(link);
    fetch(link.href, fetchOpts);
  }
})();

if you want the same effect, you can configure interop: 'auto' like this

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import createExternal from 'vite-plugin-external';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react({
      jsxRuntime: 'classic',
    }),
    createExternal({
      interop: 'auto',
      externals(libName) {
        if (libName === 'react') {
          return 'React';
        }
        if (libName === 'react-dom/client') {
          return 'ReactDOM';
        }
      },
    }),
  ],
});

fengxinming avatar Apr 02 '25 15:04 fengxinming

if you want the same effect, you can configure interop: 'auto' like this

Yeah, that works. Thank you.

I think we can mark this issue as implemented and likewise #5.

irshadahmad21 avatar Apr 02 '25 16:04 irshadahmad21

@irshadahmad21 Yes, Actually I had never found a suitable way to implement this feature until vite released version 6.x, which made some refactorings to the internal implementation of the plugin so I could better obtain pre-bundle deps.

fengxinming avatar Apr 02 '25 17:04 fengxinming

Thank you for working on it.

irshadahmad21 avatar Apr 02 '25 17:04 irshadahmad21