vite-plugin-external: Allow `externals` as function that is called on runtime
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 Can you provide a simple demo? I will use this demo to confirm after optimization
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());
}
Any update on this?
@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
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
Here is the demo https://stackblitz.com/edit/vite-external-demo-spgx9sye?file=vite.config.ts
- Run
npm run devto see it doesn't err because the react CDN script is commented out inindex.html - Run
npm run buildto see it bundles thereactpackage
Notice the bundle size, which shouldn't be more than 3-4 kB
@irshadahmad21
Upgrade [email protected] and [email protected] to fix this issue.
See https://stackblitz.com/edit/vite-external-demo-wuflfzcl?file=index.html
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 “/”.
I think that other issue is related to #5.
@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.
@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]" />
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 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';
}
},
}),
],
});
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 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.
Thank you for working on it.