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

Respect import maps #2483

Open
lubomirblazekcz opened this issue Mar 12, 2021 · 26 comments
Open

Respect import maps #2483

lubomirblazekcz opened this issue Mar 12, 2021 · 26 comments
Labels
enhancement New feature or request has workaround

Comments

@lubomirblazekcz
Copy link
Contributor

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.

@yyx990803
Copy link
Member

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 yyx990803 changed the title Ability to disable node_modules resolve Respect import maps Mar 15, 2021
@yyx990803 yyx990803 added the enhancement New feature or request label Mar 15, 2021
@lubomirblazekcz
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

@maicol07
Copy link

maicol07 commented Sep 5, 2021

@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\vite@2.5.1\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\vite@2.5.1\node_modules\vite\dist\node\chunks\dep-972722fa.js:51149:19)
    at normalizeUrl (C:\Users\Maicol\Documents\Progetti\Web\osm_rewrite\node_modules\.pnpm\vite@2.5.1\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\vite@2.5.1\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\vite@2.5.1\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\vite@2.5.1\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\vite@2.5.1\node_modules\vite\dist\node\chunks\dep-972722fa.js:67225:32

@canadaduane
Copy link

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?

@dutchcelt
Copy link

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

@ijandc
Copy link

ijandc commented Mar 20, 2023

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

@noherczeg
Copy link

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

@patak-dev
Copy link
Member

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.

@vctqs1
Copy link

vctqs1 commented May 31, 2023

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/react@17.0.2-fix.0/esm/react.development.js",
},
{
    find: new RegExp("^react-dom$"),
    replacement:
        "https://cdn.jsdelivr.net/npm/@esm-bundle/react-dom@17.0.2-fix.0/esm/react-dom.development.min.js",
},
optimizeDeps: {
    exclude: [],
    esbuildOptions: {
        plugins: [EsmExternals({ externals: ['react', 'react-dom'] })]
    },
},

@MilanKovacic
Copy link

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
Copy link

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?

@turnerguo
Copy link

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.

@MilanKovacic
Copy link

MilanKovacic commented Nov 24, 2023

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 MilanKovacic/vite-plugin-externalize-dependencies#65 is implemented.
I am open to contributions on this feature.

EDIT: new version has been released.

@noherczeg
Copy link

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

@cimchd
Copy link

cimchd commented Dec 24, 2023

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/react@17.0.2-fix.0/esm/react.development.js",
},
{
    find: new RegExp("^react-dom$"),
    replacement:
        "https://cdn.jsdelivr.net/npm/@esm-bundle/react-dom@17.0.2-fix.0/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.

@vctqs1
Copy link

vctqs1 commented Dec 24, 2023

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

@MilanKovacic
Copy link

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",
      ],
    }),
  ],
});

@vctqs1
Copy link

vctqs1 commented Jan 5, 2024

@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

@MilanKovacic
Copy link

MilanKovacic commented Jan 5, 2024

@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
      ],
    }),
  ],
});

@vctqs1
Copy link

vctqs1 commented Jan 5, 2024

@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

@roydukkey
Copy link

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`,
        }),
});

@chrisabrams
Copy link

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?

@roydukkey
Copy link

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?

@chrisabrams
Copy link

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.

@MMR-Seedbury
Copy link

MMR-Seedbury commented May 16, 2024

If import map support ever makes it into the project officially, I hope it can be toggled off via config. We currently have several projects with a rather unorthodox development environment, and rely on Vite ignoring import maps entirely both during dev and at build time. For future-proofing's sake I've already authored a tiny internal plugin to scrub all import maps from our entry points at build time, but the dev server's a different story.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request has workaround
Projects
None yet
Development

No branches or pull requests