webpack-contrib / expose-loader

Expose Loader

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Doesn't work as expected when exposing @apollo/client

GuySartorelli opened this issue · comments

Bug report

When trying to expose @apollo/client, I get an error

Actual Behavior

The browser dev console shows this error:

Uncaught ReferenceError: require is not defined index-exposed.js:1

The output of index-exposed.js in the dev console is:

var ___EXPOSE_LOADER_IMPORT___ = require("-!./index.js");
var ___EXPOSE_LOADER_GET_GLOBAL_THIS___ = require("../../expose-loader/dist/runtime/getGlobalThis.js");
var ___EXPOSE_LOADER_GLOBAL_THIS___ = ___EXPOSE_LOADER_GET_GLOBAL_THIS___;
if (typeof ___EXPOSE_LOADER_GLOBAL_THIS___["ApolloClient"] === 'undefined') ___EXPOSE_LOADER_GLOBAL_THIS___["ApolloClient"] = ___EXPOSE_LOADER_IMPORT___;
else throw new Error('[exposes-loader] The "ApolloClient" value exists in the global scope, it may not be safe to overwrite it, use the "override" option')
module.exports = ___EXPOSE_LOADER_IMPORT___;

Note that I have other modules being exposed immediately before and after @apollo/client and they all work fine (when @apollo/client isn't being exposed) - though none of them have an @ or / in them

Expected Behavior

The @apollo/client module should be exposed in the window object like other exposed modules

How Do We Reproduce?

Use the following expose line in a js file, and then transpile it using webpack:

import 'expose-loader?exposes=ApolloClient!@apollo/client';

Please paste the results of npx webpack-cli info here, and mention other relevant information

System:
OS: Linux 5.15 Ubuntu 22.04.1 LTS 22.04.1 LTS (Jammy Jellyfish)
CPU: (12) x64 Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
Memory: 5.94 GB / 15.25 GB
Binaries:
Node: 18.12.0 - ~/.nvm/versions/node/v18.12.0/bin/node
Yarn: 1.22.19 - ~/.nvm/versions/node/v18.12.0/bin/yarn
npm: 8.19.3 - ~/projects/build-stack-update/node_modules/.bin/npm
Browsers:
Firefox: 106.0.3
Packages:
copy-webpack-plugin: ^11.0.0 => 11.0.0
html-loader: ^4.2.0 => 4.2.0
markdown-loader: ^8.0.0 => 8.0.0
remove-files-webpack-plugin: ^1.5.0 => 1.5.0
webpack: ^5.74.0 => 5.74.0
webpack-cli: ^4.10.0 => 4.10.0
webpack-sources: ^3.2.3 => 3.2.3

Sounds like a bug, feel free to send a PR with fix

I haven't looked at the code for this at all but I am very much a fledgling when it comes to webpack. I wouldn't know where to start looking for the cause let alone a solution 😅

I think the bug somewhere here https://github.com/webpack-contrib/expose-loader/blob/master/src/index.js#L51, just add console.log and look at values

Awesome, thanks. If I have time to look into this I'll see what I can see

I've had a bit of a look at this - from what I can see, @apollo/client is treated the exact same as everything else - the code variable has the same structure for this as it does for everything else.

I'm not sure where callback comes from or what it does - I'm guessing that's from webpack itself somehow? That seems to be what isn't handling the generated code correctly.
My output vendor.js file contains the entries for all modules exposed before apollo client, but doesn't include it for apollo or anything exposed afterward.

I'm not sure how to debug this further at this point. For now I think I'm going to have to just not expose it in my project but hopefully someone else can either find the problem or help me see the next step.

Maybe you can create small reproducible repo?

Sure thing! I've put together a repo at https://github.com/GuySartorelli/expose-loader-issue-188 which is a simplified version of my config which can be used to reproduce the problem (just open up the index.html file in your browser and look at the error in the dev console)

Yeah, found couple problems. I setup it using:

module: {
            rules: [
                {
                    type: 'javascript/auto',
                    test: /@apollo\/client/,
                    loader: "expose-loader",
                    issuer: /vendor\.js/,
                    options: {
                        exposes: {
                            globalName: "ApolloClient",
                            override: true,
                        },
                    },
                },
            ],
        },

and using (in vendor.js):

import { ApolloClient } from '@apollo/client';

console.log(ApolloClient)

Anyway we have two problems here:

  1. First - ESM, webpack thinking that wrapped module in ESM too, so doesn't parser require, we should generate ESM code in such sutiation
  2. But here is the other problem - how reexport them (especial named export)

Another solution:

import 'virtual.js!=!expose-loader?exposes=ApolloClient!@apollo/client';

i.e. you tell webpack to create virtual module (name can be any) and handle them in javascript/auto mode (i.e. webpack tried to parser ESM then Script type).

But you need to use window.ApolloClient.ApolloCache to get instance.

We can improve those cases, but want to get feedback from you

This exact setup works with other modules, so the webpack config setup is a workaround at best. Like I mentioned originally, my actual configuration includes a bunch of other modules being exposed before and after apollo, and they all work if apollo isn't being exposed. If apollo is being exposed, then only the ones before apollo work.

The webpack config is in a shared repository, which doesn't necessarily know what modules need to be exposed - for that reason, using webpack config like in your example won't really work for my scenario. It's good to know there's a workaround that might for other people though.

The virtual.js workaround won't really work for me either, since the library which is exporting apollo may be used by third parties, and it would be an anti pattern to tell them "hey if you're using apollo, don't import it - use window.ApolloClient.ApolloCache instead.
Currently the advise is to use webpack externals - though obviously we don't include the externals configuration for the module which is responsible for doing the exposing.

The virtual.js workaround won't really work for me either, since the library which is exporting apollo may be used by third parties, and it would be an anti pattern to tell them "hey if you're using apollo, don't import it - use window.ApolloClient.ApolloCache instead.

We can fix it and attach ApolloCache to window.ApolloClient, I just want to verify using import 'virtual.js!=!expose-loader?exposes=ApolloClient!@apollo/client'; is good for you, as I written above we generate commonjs syntax here and that is why you have window.ApolloClient.ApolloCache (we just reaxport the whole module by default) instead window.ApolloClient.ApolloCache, but I consider it is a bug/feature request

Ahh sorry, I hadn't noticed you mentioning ApolloCache.

I'll be honest, I don't understand the difference between ESM or commonjs, nor do I know enough about how importing works or how @apollo/client is set up to know if what you're proposing would work for me.

But if it would mean I no longer have that error, and my imports from the module which exposes apollo work correctly, as well as any imports from other modules which have set up the following externals in their webpack.config, then that sounds like it'd be perfect.

// webpack.config.js
{
  //....
  externals: {
    '@apollo/client': 'ApolloClient',
  },
}

// some js file
import { /*anything*/ } from '@apollo/client';
import { /*other things*/} from '@apollo/client/react-hoc';
// etc

Thank you for looking into this, I really appreciate it.

@GuySartorelli Hm, I just tried import 'virtual.js!=!expose-loader?exposes=ApolloClient|ApolloClient!@apollo/client'; and looks like it works correctly, can you check?

Thank you @alexander-akait, I've tried that now and it works perfectly. Can't say I understand what's actually happening there, but so long as it works I'm happy :p

I've updated my example repo with a naive config using externals and I've applied the virtual.js workaround in the new with-workaround branch which shows it working.

There is no magic:

  1. We create virtual module, so webpak try to parser it as esm then cjs
  2. When we say expose ApolloClient using ApolloClient named export from @apollo/client package

😄

Ahh okay. I'm gonna have to read up on virtual modules some time. For now you've given me a way forward, so thank you!

Hi @alexander-akait I'm having a similar issue here where webpack doesn't replace the require and browser complains.

In the reproduction repo from @GuySartorelli you can fix the issue by just removing the "type": "module" from expose-loader-issue-188/node_modules/@apollo/client/package.json then the require are replaced properly:
image

do you have any idea why that type property affects webpack? In my case is another library but removing as well the type fixes the issue

@angel-git because modules have other exports, that is why you have different results

@alexander-akait do you have any idea what would be the solution to make these kind of dependencies to work?

@angel-git Do you this solution - #188 (comment)?

@alexander-akait Looks like doesn't work for me, i tried with:

import 'expose-loader?exposes=DotComponents|DotComponents!@digital-ai/dot-components';

and

import 'expose-loader?exposes=DotComponents!@digital-ai/dot-components';

but i get all the time the require error:

Uncaught ReferenceError: require is not defined
    at eval (index.esm-exposed.js?./node_modules/expose-loader/dist/cjs.js?exposes=DotComponents%7CDotComponents:2:34)
    at ./node_modules/expose-loader/dist/cjs.js?exposes=DotComponents|DotComponents!./node_modules/@digital-ai/dot-components/index.esm-exposed.js (vendor.js:6880:1)

source from webpack:

__webpack_require__.r(__webpack_exports__);
var ___EXPOSE_LOADER_IMPORT___ = require("-!./index.esm.js");
var ___EXPOSE_LOADER_GET_GLOBAL_THIS___ = require("../../expose-loader/dist/runtime/getGlobalThis.js");
var ___EXPOSE_LOADER_GLOBAL_THIS___ = ___EXPOSE_LOADER_GET_GLOBAL_THIS___;
var ___EXPOSE_LOADER_IMPORT_MODULE_LOCAL_NAME___ = ___EXPOSE_LOADER_IMPORT___.DotComponents
if (typeof ___EXPOSE_LOADER_GLOBAL_THIS___["DotComponents"] === 'undefined')
    ___EXPOSE_LOADER_GLOBAL_THIS___["DotComponents"] = ___EXPOSE_LOADER_IMPORT_MODULE_LOCAL_NAME___;
else
    throw new Error('[exposes-loader] The "DotComponents" value exists in the global scope, it may not be safe to overwrite it, use the "override" option')
module.exports = ___EXPOSE_LOADER_IMPORT___;

Here the repo: https://github.com/angel-git/expose-loader-issue-188/

@angel-git Sorry for delay, in your case you need to remove '@apollo/client': 'ApolloClient',, you don't need it

I want to close because resolved, anyway feel free to feedback

client

No worries for delay, glad you replied :)

I'm sorry im not sure what you mean by removing that '@apollo/client': 'ApolloClient' , since i don't have that on my repo

Oh, wrong repo tests, give me a second to check it again, sorry 😄

@angel-git Just use

import 'my-module.js!=!expose-loader?exposes=DotComponents|DotComponents!@digital-ai/dot-components';

Otherwise due to inline syntax webpack just ignores the file, because there is no module name and can't undestand that need to parse/handle this file

my-module.js can have any name, but should have .js/.cjs/.mjs as an extension

@alexander-akait that worked, thanks!