vite icon indicating copy to clipboard operation
vite copied to clipboard

Respect import maps

Open lubomirblazekcz opened this issue 3 years ago • 26 comments

Is your feature request related to a problem? Please describe. Currently it's not possible to use importmaps with Vite, because vite resolves all node_modules by default. So if I would want to use import maps with modules on cdn, rather then node_modules, I cannot.

Describe the solution you'd like Easy on/off switch, resolve.node false

I know main advantage of Vite is transforming node_modules, but having option to also use benefits of using esm modules from cdn with importmaps is also great.

lubomirblazekcz avatar Mar 12 '21 09:03 lubomirblazekcz

I think the better way to deal with this is to respect import maps when parsing index.html and avoid rewriting URLs listed in the import map.

Also, if you want to import from CDNS, you can use alias and point them to CDN urls instead of using import maps for now. FYI Import maps currently only works in latest version of Chrome, while Vite aliases will work for all browsers when built.

yyx990803 avatar Mar 15 '21 15:03 yyx990803

It can be easily polyfilled with https://www.npmjs.com/package/es-module-shims though, but I agree Vite aliases might be better for production. But Vite respecting the import maps would be also great.

lubomirblazekcz avatar Mar 15 '21 16:03 lubomirblazekcz

For anyone interested in using importmaps with Vite right now, you can with workaround. When es-module-shims polyfill is used with type="importmap-shim" and type="module-shim", all the imports will be handled with the shim.

lubomirblazekcz avatar May 02 '21 10:05 lubomirblazekcz

@lubomirblazekcz I've tried the polyfill but it doesn't work with the dev server since the analyzer plugin throws an error:

[plugin:vite:import-analysis] Failed to resolve import "modulePage" from "resources\js\app.js". Does the file exist?
C:/Users/Maicol/Documents/Progetti/Web/osm_rewrite/resources/js/app.js:6:50
4  |  
5  |  // eslint-disable-next-line import/no-unresolved
6  |  import /* @vite-ignore */ * as modulePages from 'modulePage';
   |                                                   ^
7  |  
8  |  import '../scss/app.scss';
    at formatError (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:51153:46)
    at TransformContext.error (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:51149:19)
    at normalizeUrl (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:74554:26)
    at async TransformContext.transform (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:74687:57)
    at async Object.transform (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:51354:30)
    at async transformRequest (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:67098:29)
    at async viteTransformMiddleware (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\[email protected]\node_modules\vite\dist\node\chunks\dep-972722fa.js:67225:32

maicol07 avatar Sep 05 '21 12:09 maicol07

For anyone interested in using importmaps with Vite right now, you can with workaround. When es-module-shims polyfill is used with type="importmap-shim" and type="module-shim", all the imports will be handled with the shim.

This is true, but it has the unfortunate side-effect that code inside a regular module script tag can't access code inside the importmap-shim script tag. Unless you've found a way that they can talk?

canadaduane avatar Nov 19 '21 06:11 canadaduane

Does anybody know if there's any progress on this?

dutchcelt avatar Feb 18 '23 16:02 dutchcelt

Does anyone know if there's any progress on this?

ijandc avatar Mar 20 '23 03:03 ijandc

I'm also interested in plans / potential progress / roadmap for this.

noherczeg avatar May 25 '23 12:05 noherczeg

We discussed this feature in a recent team meeting and we decided that the best path forward is:

  • Vite will leave import maps definitions untouched.
  • A new external config option will be implemented, that allows to define that an id is external for both dev and build.
  • Users will need to add all the entry ids from import maps to this new external config option.

Vite has a single module graph for its server. If you have two modules imported by different HTML entry points, they will be a single module node in graph. Vite could automatically read the import map and respect it without external but that will be a big change in the conceptual model of Vite dev. Every module should start having a ?entry=path.html query for example. We are also able to request a module in isolation, but this will break. Having the entries of the import map as external avoids these issues.

patak-dev avatar May 25 '23 13:05 patak-dev

I'm also encountering same issue to workaround

(for EX: in my case, i'd like to external react/react-dom)

  • set resolve.alias
{
    find: new RegExp("^react$"),
    replacement:
        "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/esm/react.development.js",
},
{
    find: new RegExp("^react-dom$"),
    replacement:
        "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/esm/react-dom.development.min.js",
},
  • set external in esbuild for dev mode as well, via plugin https://github.com/remorses/esbuild-plugins/tree/master/esm-externals#esbuild-pluginsesm-externals (but in my case, in dev mode, I still would like to resolve react/jsx-dev-runtime and jsx/runtime instead external them all, so I already create PR to address this issue here https://github.com/remorses/esbuild-plugins/pull/39)
optimizeDeps: {
    exclude: [],
    esbuildOptions: {
        plugins: [EsmExternals({ externals: ['react', 'react-dom'] })]
    },
},

vctqs1 avatar May 31 '23 13:05 vctqs1

I have created a plugin to solve this issue during development, until official support is implemented:

https://github.com/MilanKovacic/vite-plugin-externalize-dependencies

MilanKovacic avatar Aug 08 '23 09:08 MilanKovacic

We discussed this feature in a recent team meeting and we decided that the best path forward is:

  • Vite will leave import maps definitions untouched.
  • A new external config option will be implemented, that allows to define that an id is external for both dev and build.
  • Users will need to add all the entry ids from import maps to this new external config option.

Vite has a single module graph for its server. If you have two modules imported by different HTML entry points, they will be a single module node in graph. Vite could automatically read the import map and respect it without external but that will be a big change in the conceptual model of Vite dev. Every module should start having a ?entry=path.html query for example. We are also able to request a module in isolation, but this will break. Having the entries of the import map as external avoids these issues.

Hi @patak-dev, any progress on this?

MilanKovacic avatar Aug 27 '23 00:08 MilanKovacic

I have created a plugin to solve this issue during development, until official support is implemented:

https://github.com/MilanKovacic/vite-plugin-externalize-dependencies

@MilanKovacic thanks for your plugin but seems not work, as @vctqs1 said it will broken in dev mode, since vite still resolve react/jsx-dev-runtime.

turnerguo avatar Nov 24 '23 04:11 turnerguo

I have created a plugin to solve this issue during development, until official support is implemented: https://github.com/MilanKovacic/vite-plugin-externalize-dependencies

@MilanKovacic thanks for your plugin but seems not work, as @vctqs1 said it will broken in dev mode, since vite still resolve react/jsx-dev-runtime.

Hi, it works in development mode, you just have to specify all of the exports, for example: ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"].

This will not be necessary once https://github.com/MilanKovacic/vite-plugin-externalize-dependencies/issues/65 is implemented. I am open to contributions on this feature.

EDIT: new version has been released.

MilanKovacic avatar Nov 24 '23 10:11 MilanKovacic

If you want to externalize all dependenciesthen you can also read package.json and parse out the keys.

noherczeg avatar Nov 24 '23 11:11 noherczeg

I'm also encountering same issue to workaround

(for EX: in my case, i'd like to external react/react-dom)

  • set resolve.alias
{
    find: new RegExp("^react$"),
    replacement:
        "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/esm/react.development.js",
},
{
    find: new RegExp("^react-dom$"),
    replacement:
        "https://cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/esm/react-dom.development.min.js",
},
optimizeDeps: {
    exclude: [],
    esbuildOptions: {
        plugins: [EsmExternals({ externals: ['react', 'react-dom'] })]
    },
},

@vctqs1 Can you provide your vite config? I have the same problem with react but somehow your workaround won't work for me.

cimchd avatar Dec 24 '23 07:12 cimchd

import type { Plugin } from "esbuild";

export function esbuildPluginESMExternals({
    buildName,
    externals,
}: {
    buildName: string;
    externals: (string | RegExp)[];
}): Plugin {
    const namespace = `ns-esbuild-plugin-esm-externals-${buildName}`;

    const regexList = externals.map((external) =>
        external instanceof RegExp ? `(${external.source})` : `(${external})`
    );

    //Example: ^((react)|(react-dom))$
    const filter = new RegExp("^(" + regexList.join("|") + ")$");

    return {
        name: "esbuild-plugin-esm-externals",
        setup(build) {
            build.onResolve({ filter: /.*/, namespace }, (args) => {
                return {
                    path: args.path,
                    external: true,
                };
            });
            build.onResolve({ filter }, (args) => {
                return {
                    path: args.path,
                    namespace,
                };
            });
            build.onLoad({ filter: /.*/, namespace }, (args) => {
                const name = args.path;

                return {
                    contents: `export * as default from "${name}"; \nexport * from "${name}";`,
                };
            });
        },
    };
}

in vite.config.ts

optimizeDeps: {
                    esbuildOptions: {
                        plugins: isDevelopmentMode
                            ? [esbuildPluginESMExternals({ buildName, externals })]
                            : [],
                    },
                },

with buildName is just application name (string), externals = ["react", "react-dom"] @cimchd

If you still encounter issues, maybe better to share your message or something, because this is not something common so you might get something different from my issue

vctqs1 avatar Dec 24 '23 15:12 vctqs1

vite-plugin-externalize-dependencies now has "full" support for externalizing modules in development:

import { defineConfig } from "vite";
import externalize from "vite-plugin-externalize-dependencies";

export default defineConfig({
  plugins: [
    externalize({
      externals: [
        "react", // Externalize "react", and all of its subexports (react/*), such as react/jsx-runtime
        /^external-.*/, // Externalize all modules starting with "external-"
        (moduleName) => moduleName.includes("external"), // Externalize all modules containing "external",
      ],
    }),
  ],
});

MilanKovacic avatar Jan 03 '24 22:01 MilanKovacic

@MilanKovacic hi, I saw your plugin, but what we want here when externals react and react-dom is exactly only externalize react and react-dom not react/* and not react/jsx-runtime also

vctqs1 avatar Jan 05 '24 03:01 vctqs1

@vctqs1 I might change the default behavior of the plugin to not automatically externalize subexports (make it configurable). For now, this can be achieved with the regex / function matching offered by the plugin.

import { defineConfig } from "vite";
import externalize from "vite-plugin-externalize-dependencies";

export default defineConfig({
  plugins: [
    externalize({
      externals: [
        /^react$/, // Externalize only the "react" module
        /^react-dom$/, // Externalize only the "react-dom" module
      ],
    }),
  ],
});

MilanKovacic avatar Jan 05 '24 09:01 MilanKovacic

@vctqs1 I might change the default behavior of the plugin to not automatically externalize subexports (make it configurable). For now, this can be achieved with the regex / function matching offered by the plugin.

import { defineConfig } from "vite";
import externalize from "vite-plugin-externalize-dependencies";

export default defineConfig({
  plugins: [
    externalize({
      externals: [
        /^react$/, // Externalize only the "react" module
        /^react-dom$/, // Externalize only the "react-dom" module
      ],
    }),
  ],
});

Im trying your code, but there is the messsage

[root-shell] [single-spa] TypeError: application 'calendar-main' died in status LOADING_SOURCE_CODE: Cannot read properties of undefined (reading 'ReactCurrentDispatcher')
    at react-jsx-dev-runtime.development.js:324:51
    at node_modules/react/cjs/react-jsx-dev-runtime.development.js (react-jsx-dev-runtime.development.js:1202:3)
    at __require (chunk-CEQRFMJQ.js?v=007daa77:11:50)
    at node_modules/react/jsx-dev-runtime.js (jsx-dev-runtime.js:6:20)
    at __require (chunk-CEQRFMJQ.js?v=007daa77:11:50)
    at jsx-dev-runtime.js:7:1 status LOAD_ERROR
_call @ index.mjs:35

vctqs1 avatar Jan 05 '24 11:01 vctqs1

Here is a plugin I wrote to get this working in dev and production.

import type { Plugin, UserConfig } from 'vite';

/**
 * Defines the document's import map and omits the specified entries from the bundle.
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap}
 * @see {@link https://github.com/vitejs/vite/issues/2483}
 */
export default (mode: string, entries: { [key: string]: string }): Plugin => ({
    name: 'importMap',
    config: () => {
        const config: UserConfig = {
            build: {
                rollupOptions: {
                    external: Object.keys(entries),
                },
            },
        };

        if (mode === 'development') {
            config.resolve = {
                alias: entries,
            };
        }

        return config;
    },
    transformIndexHtml: (html) => {
        const content = Object.entries(entries).map(([from, to]) => `"${from}":"${to}"`).join(',');
        return html.replace(/^(\s*?)<title>.*?<\/title>/m, `$&$1<script type="importmap">{"imports":{${content}}}</script>`);
    },
});

And, an example usage:

import { defineConfig } from 'vite';
import importMap from './vite/importMap.js';
import { version as VueVersion } from 'vue';

export default defineConfig(({ mode }) => ({
    plugins: [
        importMap(mode, {
            'vue/compiler-sfc': `https://unpkg.com/@vue/compiler-sfc@${VueVersion}/dist/compiler-sfc.esm-browser.js`,
            'vue': `https://unpkg.com/vue@${VueVersion}/dist/vue.esm-${mode === 'development' ? 'browser' : 'browser.prod'}.js`,
        }),
});

roydukkey avatar Jan 26 '24 20:01 roydukkey

Here is a plugin I wrote to get this working in dev and production.

import type { Plugin, UserConfig } from 'vite';

/**
 * Defines the document's import map and omits the specified entries from the bundle.
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap}
 * @see {@link https://github.com/vitejs/vite/issues/2483}
 */
export default (mode: string, entries: { [key: string]: string }): Plugin => ({
    name: 'importMap',
    config: () => {
        const config: UserConfig = {
            build: {
                rollupOptions: {
                    external: Object.keys(entries),
                },
            },
        };

        if (mode === 'development') {
            config.resolve = {
                alias: entries,
            };
        }

        return config;
    },
    transformIndexHtml: (html) => {
        const content = Object.entries(entries).map(([from, to]) => `"${from}":"${to}"`).join(',');
        return html.replace(/^(\s*?)<title>.*?<\/title>/m, `$&$1<script type="importmap">{"imports":{${content}}}</script>`);
    },
});

And, an example usage:

import { defineConfig } from 'vite';
import importMap from './vite/importMap.js';
import { version as VueVersion } from 'vue';

export default defineConfig(({ mode }) => ({
    plugins: [
        importMap(mode, {
            'vue/compiler-sfc': `https://unpkg.com/@vue/compiler-sfc@${VueVersion}/dist/compiler-sfc.esm-browser.js`,
            'vue': `https://unpkg.com/vue@${VueVersion}/dist/vue.esm-${mode === 'development' ? 'browser' : 'browser.prod'}.js`,
        }),
});

Thanks for sharing @roydukkey . The code you provided does work, however, I noticed my HMR stopped working because of the usage of config.resolve. If I disable this:

config.resolve = {
  alias: entries,
}

then HMR continues to work. Any idea why config.resolve would break HMR?

chrisabrams avatar Jan 31 '24 21:01 chrisabrams

Oh. Interesting. I found that I needed to use config.resolve.alias with the entires in order to keep dev environment working. Does HMR break for the imports you have listed as entires, or is it breaking somewhere else?

roydukkey avatar Jan 31 '24 22:01 roydukkey

Oh. Interesting. I found that I needed to use config.resolve.alias with the entires in order to keep dev environment working. Does HMR break for the imports you have listed as entires, or is it breaking somewhere else?

Looking at this more, the issue does not appear to be anything related to your plugin. If I create a new Vite project, and simply add resolve: {alias: {}} to the default Vite config, HMR will stop working.

chrisabrams avatar Jan 31 '24 22:01 chrisabrams