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

Support ES module import in browser #53

Closed
DanielHabenicht opened this issue Apr 29, 2022 · 65 comments · Fixed by #123
Closed

Support ES module import in browser #53

DanielHabenicht opened this issue Apr 29, 2022 · 65 comments · Fixed by #123
Labels
enhancement New feature or enhancement help wanted Community assistance is most appreciated

Comments

@DanielHabenicht
Copy link

Hi again,

I tested the library with angular and it is running into an error:

universalModuleDefinition:1 Uncaught ReferenceError: global is not defined
    at universalModuleDefinition:1:1
    at Object.9424 (universalModuleDefinition:1:1)
    at __webpack_require__ (bootstrap:19:1)
    at Module.1896 (home.component.html:14:263)
    at __webpack_require__ (bootstrap:19:1)
    at Module.721 (universalModuleDefinition:1:1)
    at __webpack_require__ (bootstrap:19:1)
    at Module.23 (app.component.html:6:8)
    at __webpack_require__ (bootstrap:19:1)
    at Module.8835 (environment.ts:16:71)

Which is expected as the angular team came to the conclusion to not support global in their webpack build for broad compatibility. angular/angular-cli#9827 (comment)

I think this library should not rely on some other process to supply the variable.
So going forward you could use:

Workaround is adding (window as any).global = window; to polyfill.ts

@elringus
Copy link
Owner

Is this about this line: https://github.com/Elringus/DotNetJS/blob/cad9a87854fd860203d25991a55bb3a68fa11229/DotNet/Packer/LibraryGenerator/LibraryTemplate.cs#L11 ?

It should only invoke in node environment, so global should be accessible.

@elringus
Copy link
Owner

If it's that line, can you please test if the following will work with angular:

factory(module.exports, global || window); 

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

It does not work, because global is not defined (so it produces the same error). You have to check if it exists before:

// Get global shim as in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis#search_for_the_global_across_environments
var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

factory(module.exports, getGlobal()); 

@elringus
Copy link
Owner

Wouldn't global || window return window in case global is not defined?

@elringus
Copy link
Owner

Or should it be global ?? window.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

You can try it out in your own web browser, just open the Developer Tools and navigate to the console:
image

It's kind of unsuspecting, because I would have said the same before I tried it out.

@elringus
Copy link
Owner

Do you have any idea how we can make an automated test for this without involving angular?

@DanielHabenicht
Copy link
Author

Execute the module in a browser context.
This could be a way: https://www.sitepoint.com/using-es-modules/#:~:text=Safari%2C%20Chrome%2C%20Firefox%20and%20Edge,Here's%20what%20they%20look%20like.&text=Simply%20add%20type%3D%22module%22,executing%20each%20module%20only%20once.

Otherwise I think there is no way around implementing it inside an example project.
Which would also be a way to test it.

@elringus
Copy link
Owner

Wait, but it does already work in browser (there is a sample project). The line with global shouldn't be invoked in browser anyway.

@DanielHabenicht
Copy link
Author

For webpack projects it is invoked because they are loaded as module (independent of later being executed in the browser).
I think it would be the same for react or any other webframwork.

@elringus
Copy link
Owner

I'm currently using the library in react, no issues here.

@DanielHabenicht
Copy link
Author

Might be that react is already polyfilling global?

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

I tried it out with stencil now, other error:

index-c37bab2d.js:2903 TypeError: Cannot set properties of undefined (setting 'dotnet')
    at app-home.entry.js:10:21
    at app-home.entry.js:11:3 undefined

Happen with or without the getGlobal fix.

import { Component, h } from '@stencil/core';
import * as dotnet from '../../../../Project/bin/dotnet';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css',
  shadow: true,
})
export class AppHome {
  componentDidLoad() {
    dotnet.boot();
  }
  render() {
     ...
  }
}

@elringus
Copy link
Owner

I think I was able to reproduce this in hello world sample with the browser module as you've suggested:

<!DOCTYPE html>

Open console (Ctrl+Shift+I) to see the output.


<script type="module">
    import * as dotnet from './Project/bin/dotnet.js'

    dotnet.HelloWorld.GetHostName = () => "Browser";

    window.onload = async function () {
        await dotnet.boot();
        const guestName = dotnet.HelloWorld.GetName();
        console.log(`Welcome, ${guestName}! Enjoy your global space.`);
    };

</script>

— but using the getGlobal() doesn't seem to help:

    var getGlobal = function () {
        if (typeof self !== 'undefined') { return self; }
        if (typeof window !== 'undefined') { return window; }
        if (typeof global !== 'undefined') { return global; }
        throw new Error('unable to locate global object');
    };
    if (typeof exports === 'object' && typeof exports.nodeName !== 'string')
        factory(module.exports, getGlobal());
    else factory(root.dotnet, root);

@elringus
Copy link
Owner

The error seem to come from webpackUniversalModuleDefinition; could it be caused by the webpack autogenerated UMD wrapper?

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

I think this is related: umdjs/umd#127 and umdjs/umd#134

a comment suggests to switch to esm, but I don't now if this is compatible with VSCode related targets

@DanielHabenicht
Copy link
Author

Seems like the only way around is publishing two js files. One for ES modules and one as UMD.

@elringus elringus changed the title Usage of global leads to error in Angular Environment Support ES module import in browser Apr 29, 2022
@elringus elringus added enhancement New feature or enhancement help wanted Community assistance is most appreciated labels Apr 29, 2022
@elringus
Copy link
Owner

I've added browser-es sample for testing (not working right now): https://github.com/Elringus/DotNetJS/blob/main/Samples/HelloWorld/browser-es.html

Not sure what's the best approach to solve this (producing 2 libraries is not optimal, as it will require a lot of extra logic in C# packer). Guess the best workaround for now is polyfilling global on the consumer side. Will appreciate any suggestions.

@elringus elringus added this to the First stable release milestone Apr 29, 2022
@elringus
Copy link
Owner

Might be that react is already polyfilling global?

More likely it's just I'm bundling the react app with webpack, which statically links all imports. If I got it right, this issue only matters when you try to directly import dotnet in browser as ES module. You can still use it with any framework or inside any app and then bundle it.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

Angular also uses Webpack under the hood.
But alright you already changed the title of the issue to reflect on the ES module issue.
Appreciate your investiagation.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

Well React is using kind of UMD only: facebook/react#10021

@elringus
Copy link
Owner

Maybe angular is bundled differently, without static linking the deps? I mean, if bundle already has all the imports resolved/linked internally at build time, it shouldn't matter which import mechanism each individual dependency uses when the bundle is imported at runtime, right?

@DanielHabenicht
Copy link
Author

Well yes angular is already using esm modules.
Would you be open to including the polyfill for getGlobal anyway?

@elringus
Copy link
Owner

Would you be open to including the polyfill for getGlobal anyway?

If it'll solve the issue, sure. I've already tried doing that actually, but it didn't work, because we also need to share that global with dotnet/blazor internal stuff. But maybe I've missed something.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented Apr 29, 2022

If it'll solve the issue, sure. I've already tried doing that actually, but it didn't work, because we also need to share that global with dotnet/blazor internal stuff. But maybe I've missed something.

It is at least solving the issue for angular, as angular seems to use a different way for importing the module.

For importing the module via type"module" we would have to set this to window conditionally in dotnet.js:

!(function (e, t) {
    "object" == typeof exports && "object" == typeof module
        ? (module.exports = t())
        : "function" == typeof define && define.amd
        ? define([], t)
        : "object" == typeof exports
        ? (exports.dotnet = t())
        : (e.dotnet = t());
})(this, () => // <== Here
...

Then it is available via window.global.dotnet

@elringus
Copy link
Owner

I've tried replacing this to window, but it returned Uncaught TypeError: dotnet.HelloWorld is undefined.

Regarding angular, do you mean it's not related with the browser-es sample? In this case we'd need a minimal repro (without angular) as a starting point. Ideally, an automated test or at least a sample for manual testing.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented May 1, 2022

The "problem" with web modules (ES) is that they are incompatible with older package runtimes as they add the export keyword which may not be supported.

So, I support @Aloento view of generating an ES module first and then using rollup to create UMD/CommonJs, because generating them from ES modules is easy whereas generating ES modules from UMD is not feasible.

But in the end this is way more work than any of us should invest for this problem.
The great thing is I found this PR: umdjs/umd#124
Which suggests the second edit:

+var getGlobal = function () {
+    if (typeof self !== "undefined") {
+        return self;
+    }
+    if (typeof window !== "undefined") {
+        return window;
+    }
+    if (typeof global !== "undefined") {
+        return global;
+    }
+    throw new Error("unable to locate global object");
+};
+var global = getGlobal();
...
-})(this, () =>
+})(typeof self !== "undefined" ? self : this, () =>

Making it work in angular, and at least loading as web module:

<script type="module">
    import * as dotnet from "./Project/bin/dotnet.js";

    window.onload = async function () {
        await window.dotnet.boot();
        const guestName = window.dotnet.HelloWorld.GetName();
        console.log(`Welcome, ${guestName}! Enjoy your global space.`);
    };
</script>

But running into an error:

dotnet.js:10763 Uncaught (in promise) ReferenceError: bodyJs is not defined
    at Object.bind_method (dotnet.js:10763:49)
    at s (dotnet.js:8876:64)
    at Object.bindings_lazy_init (dotnet.js:8887:49)
    at Object.bind_static_method (dotnet.js:10849:42)
    at p (dotnet.js:13734:38)
    at boot (dotnet.js:13980:34)
    at async Object.exports.boot (dotnet.js:14138:9)
    at async window.onload (global-module.html:9:9)

@elringus
Copy link
Owner

elringus commented May 1, 2022

Making it work in angular

Have you tried [JSFunction] stuff? I guess it won't work there as well. Mono-wasm requires a globally-accessible variable to invoke funcs in JS and looks like while this change allows routing the global into the module it's not persisting the assigned objects back.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented May 1, 2022

Making it work in angular

Have you tried [JSFunction] stuff? I guess it won't work there as well. Mono-wasm requires a globally-accessible variable to invoke funcs in JS and looks like while this change allows routing the global into the module it's not persisting the assigned objects back.

Yes, I loaded your example HelloWorld Sample.

@elringus
Copy link
Owner

elringus commented May 1, 2022

Then I'm probably doing something wrong. I've tried using the getGlobal and self -> this stuff, but it still didn't work with the browser-es example. It's booting but assigned functions are not resolved by the runtime.

@DanielHabenicht
Copy link
Author

DanielHabenicht commented May 1, 2022

maybe it did not come clear from my comment:

and at least loading as web module:

which means I am running into the same errors with your example, like bodyJs is not defined (on execution of boot(). But at least the loading of the module itself is not erroring.
I think this maybe has to with web modules effectively enforcing use strict.

@elringus
Copy link
Owner

elringus commented May 1, 2022

Ok, so I've experimented a bit with migrating the library to es module; rewritten all the JS tests to es-style imports and switched webpack to module output: https://github.com/Elringus/DotNetJS/tree/feat/es-module/JavaScript/dotnet-runtime

I'm currently stuck on how to better wrap the webpack-exported library with the C# packer. Previously I've just copy-pasted the UMD template, but now we have to somehow wrap/override the modules exported by webpack. I'm not familiar enough with all that stuff, so any suggestions are most welcome.

@Aloento
Copy link

Aloento commented May 2, 2022

webpack programmatic api is very useful
https://webpack.js.org/api/node/

And Microsoft.AspNetCore.NodeServices can help you to call NodeJS in C#
I don't see anything wrong with calling Webpack via MSBuild at compile-time, but it's depends on you.

@elringus
Copy link
Owner

elringus commented May 2, 2022

This will require installing/running webpack when publishing C# project. I'd like to stick with manual patching if possible to simplify the process.

@elringus
Copy link
Owner

elringus commented May 3, 2022

It seems manual patching is not feasible after all, given the mess wepback produce with the module target.

Will investigate AspNetCore.NodeServices.

@elringus
Copy link
Owner

elringus commented May 3, 2022

Ah, looks like it's deprecated. dotnet/aspnetcore#12890

@Aloento
Copy link

Aloento commented May 9, 2022

Let me work on this for your esm

@elringus
Copy link
Owner

elringus commented May 9, 2022

You mean AspNetCore.NodeServices? Given it's deprecated, I don't think it's a good idea to rely on this lib.

@Aloento
Copy link

Aloento commented May 9, 2022

You mean AspNetCore.NodeServices? Given it's deprecated, I don't think it's a good idea to rely on this lib.

I am now working on understanding your dotnet-runtime and modifying it for use.
And I'm confused about something...

@Aloento
Copy link

Aloento commented May 9, 2022

PR, please check it

@promontis
Copy link
Contributor

@elringus I have a similar problem. I'm using React with NX (webpack) and I got the global is not defined error. I override the webpack config to add global to webpack. I now also get bodyJs is not defined.

Did you manage to get it to work in React? If so, did you configure anything special?

@elringus
Copy link
Owner

I'm using it with React, yes. It's a default CRA, w/o any custom building or tooling tweaks. Builds with react-scripts build.

@promontis
Copy link
Contributor

Just tried it... it works with CRA. Had to disable ESLint for the dotnet.js file, but that's ok

@promontis
Copy link
Contributor

@elringus @Aloento do you know why we sometimes get the bodyJs is not defined while we do not get this using CRA?

@promontis
Copy link
Contributor

For NX it now works if I first create a react app using CRA, and use cra-to-nx from NX.

@tuldu
Copy link

tuldu commented Nov 20, 2022

But running into an error:

dotnet.js:10763 Uncaught (in promise) ReferenceError: bodyJs is not defined
    at Object.bind_method (dotnet.js:10763:49)
    at s (dotnet.js:8876:64)
    at Object.bindings_lazy_init (dotnet.js:8887:49)
    at Object.bind_static_method (dotnet.js:10849:42)
    at p (dotnet.js:13734:38)
    at boot (dotnet.js:13980:34)
    at async Object.exports.boot (dotnet.js:14138:9)
    at async window.onload (global-module.html:9:9)

Running into this as well for my angular project... any workaround?

@DanielHabenicht
Copy link
Author

But running into an error:

dotnet.js:10763 Uncaught (in promise) ReferenceError: bodyJs is not defined
    at Object.bind_method (dotnet.js:10763:49)
    at s (dotnet.js:8876:64)
    at Object.bindings_lazy_init (dotnet.js:8887:49)
    at Object.bind_static_method (dotnet.js:10849:42)
    at p (dotnet.js:13734:38)
    at boot (dotnet.js:13980:34)
    at async Object.exports.boot (dotnet.js:14138:9)
    at async window.onload (global-module.html:9:9)

Running into this as well for my angular project... any workaround?

Yes, try the workaround thats posted at the top the issue (inital issue description):

Workaround is adding (window as any).global = window; to polyfill.ts

@tuldu
Copy link

tuldu commented Nov 20, 2022

Yeah, adding this line didn't fix it, unfortunately :( (it did fix another error, but not this one)

@tuldu
Copy link

tuldu commented Nov 21, 2022

Debugged this a bit more. Had to manually change the syntax in the generated dotnet.js file to get it working

image

image

Is it possible to change Angular to understand this syntax, or generate code with syntax Angular can understand?

@elringus
Copy link
Owner

I'm not familiar with Angular, but regarding changing it on our side: the code comes from .NET's mono-wasm; iirc, they've improved it in .NET 7, so hopefully it'll work after we upgrade (#20). I'll also look into migrating to ES bundler after that.

@Metrowerks
Copy link

Metrowerks commented Dec 1, 2022

I'm not familiar with Angular, but regarding changing it on our side: the code comes from .NET's mono-wasm; iirc, they've improved it in .NET 7, so hopefully it'll work after we upgrade (#20). I'll also look into migrating to ES bundler after that.

It's not about Angular. It's about the type of Javascript syntax that's generated. The generated JS implies variable declaration through expressions using commas (see result in the screenshot @tuldu attached. This syntax is much less common in JS than explicitly declaring the variable with a var (as on the modified script on the right side of the screenshot). This article discusses similar topic.

Angular doesn't parse this syntax well. I think it should be changed to the more standard and common syntax regardless Angular can parse it or not.

@jerebtw
Copy link

jerebtw commented Apr 17, 2023

This is also a problem if you want to run the code in deno, since deno does not support umd.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or enhancement help wanted Community assistance is most appreciated
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants