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

CJS -> ESM interoperability for imports inside external modules loaded from CDN #32213

Closed
JayaKrishnaNamburu opened this issue Dec 7, 2021 · 10 comments
Assignees
Labels
Metadata Related to Next.js' Metadata API. please verify canary The issue should be verified against next@canary. It will be closed after 30 days of inactivity

Comments

@JayaKrishnaNamburu
Copy link

What version of Next.js are you using?

12.0.7

What version of Node.js are you using?

12.22.6 and above

What browser are you using?

Chrome

What operating system are you using?

macOS

How are you deploying your application?

next start

Describe the Bug

If a CDN esm module uses Head from next/head breaks next dev

Here is a esm module using next/head
https://jscdn.teleporthq.io/new-project-68d4/globals.js@0888a7f951f31cc0e33ee614e97d5b6e398ed19c

And it is imported inside
https://jscdn.teleporthq.io/new-project-68d4/card.js@edb67533c00984de863a24b4433f30bab9eae03e

Now, if we try to import and use the component. It breaks the render.

import styles from '../styles/Home.module.css'
import Component from 'https://jscdn.teleporthq.io/new-project-68d4/card.js@edb67533c00984de863a24b4433f30bab9eae03e'

const App = () => {

  return (
    <div className={styles.container}>
     <Component />
    </div>
  )
}

export default App

Screenshot 2021-12-07 at 8 08 54 PM

Expected Behavior

Component to load the module and render it without breaking

To Reproduce

Here is a online repl which does the same to reproduce the bug
https://replit.com/@JayaKrishnaNamb/nextjs-http#pages/index.js

GitHub repo to reproduce in local
https://github.com/teleporthq/nextjs-http

@JayaKrishnaNamburu JayaKrishnaNamburu added the bug Issue was opened via the bug report template. label Dec 7, 2021
@balazsorban44 balazsorban44 added area: URL Imports Metadata Related to Next.js' Metadata API. and removed bug Issue was opened via the bug report template. labels Dec 7, 2021
@mattcarlotta
Copy link
Contributor

mattcarlotta commented Dec 8, 2021

It appears that since next/head is a CJS file...

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.defaultHead = defaultHead;
...
var _default = Head;
exports.default = _default;

...when importing Head from a URL, import correctly resolves the dependency to an object (since CJS uses exports and not export). Therefore. expect all CJS imports to resolve exports as an object:

{
  __esModule: true,
  default: function Head(param),
  defaultHead: function defaultHead(param),
}

Moving forward, Vercel will either have to...

A.) Generate an ESM file version for all its exports and support outside ES modules (won't solve 3rd party dependencies)

B.) Or transpile all JavaScript assets over URL through Webpack's babel/swc plugins (as of now, it just appears to copy them over to next.lock/data/<http(s).domain.ext>/<filepath>_<hash> and read them as is -- unless its a supported experimental dependency)

C.) Or build an internal utility function that interpolates all import statements by checking for a default and __esModule property for URL assets...
[email protected]

import React from "react";
import Head from "next/head";

// this could be internalized and automatically
// handle "default" import statements
const interopDefault = (obj) => {
    return obj && obj.__esModule ? obj.default : obj;
}

const Globals = ()=>{
    return(/*#__PURE__*/ interopDefault(React.createElement("div", null, /*#__PURE__*/ interopDefault(React.createElement(interopDefault(Head), null, /*#__PURE__*/ interopDefault(React.createElement("link", {
        rel: "stylesheet",
        href: "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
    })))))));
};
export default Globals;

D.) Or export all of its assets as named exports and expect the developer to use the named export instead (won't solve 3rd party dependencies)...
next/head

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.defaultHead = defaultHead;
exports.Head = Head;
...
var _default = Head;
exports.default = _default;

[email protected]

import React from "react";
import { Head } from "next/head";

const Globals = ()=>{
    return(/*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement(Head, null, /*#__PURE__*/ React.createElement("link", {
        rel: "stylesheet",
        href: "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
    }))));
};
export default Globals;

E.) Or expect the developer to use the default property for any 3rd party JavaScript dependencies...
[email protected]

import React from "react";
import Head from "next/head";

const Globals = ()=>{
    return(/*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement(Head.default, null, /*#__PURE__*/ React.createElement("link", {
        rel: "stylesheet",
        href: "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
    }))));
};
export default Globals;

Unfortunately, this issue will become more widespread as a developer adds more 3rd party dependencies via URL, since import won't interpolate the CJS exports.__esModule property nor will it handle the ESM export property since it's an outside module. On the same note, this will also lead to another issue where other non JavaScript dependencies imported within a URL asset (like CSS stylesheets, images and fonts) won't be handled correctly.

@JayaKrishnaNamburu
Copy link
Author

@mattcarlotta yes, you were correct. next/head is a CJS file and so a normal imports in NEXTJS are processed with some interoperability mechanism. But the imports inside relies on node resolution. In this case, if we want to make next/head work inside globals.js file from CDN. The import should be

import React from "react";
import mod from "next/head";
const Head = mod.default

const Globals = ()=>{
    return(/*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement(Head, null, /*#__PURE__*/ React.createElement("link", {
        rel: "stylesheet",
        href: "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
    }))));
};
export default Globals;

And this renders the component without any issues.
This made things more clear that issue #31088 is also the same.
The styled-components import in the app code loads with interoperability. But the CDN module too is used styled-components but it resolution is different there.

But in that case, it was a manual workaround to force the project to load only esm version of styled-components using webpack alias.

@JayaKrishnaNamburu JayaKrishnaNamburu changed the title Head from next/head inside a http module breaks the rendering CJS -> ESM interoperability for imports inside external modules loaded from CDN Dec 8, 2021
@JayaKrishnaNamburu
Copy link
Author

JayaKrishnaNamburu commented Dec 8, 2021

And i am not sure if nextjs controls the module-resolution of it's own. Or just delegates to the node one. Because, the same example with styled-components is resolving the same irrespective of imported from the code directly or via CDN module.
Here is a example
https://codesandbox.io/s/new-project1-forked-z64zx?file=/src/teleporthq/pages/home.js

@mattcarlotta
Copy link
Contributor

mattcarlotta commented Dec 8, 2021

And i am not sure if nextjs controls the module-resolution of it's own. Or just delates to the node one. Because, the same example with styled-components is resolving the same irrespective of imported from the code directly or via CDN module. Here is a example https://codesandbox.io/s/new-project1-forked-z64zx?file=/src/teleporthq/pages/home.js

I believe styled-components works because it does something similar to what I mentioned above in Option C, where the default property is interpolated within the package itself:

styled-components.cjs.js

...
return e && "object" == typeof e && "default" in e ? e.default : e
...

styled-components.js

...
var r="default" in e ? e.default : e
...

next/dist/shared/lib/head.js (doesn't interpolate default like above)

// returns/builds the obj instead of returning just the default property
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}

// returns/builds the obj instead of returning just the default property
function _interopRequireWildcard(obj) {
    if (obj && obj.__esModule) {
        return obj;
    } else {
        var newObj = {
        };
        if (obj != null) {
            for(var key in obj){
                if (Object.prototype.hasOwnProperty.call(obj, key)) {
                    var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {
                    };
                    if (desc.get || desc.set) {
                        Object.defineProperty(newObj, key, desc);
                    } else {
                        newObj[key] = obj[key];
                    }
                }
            }
        }
        newObj.default = obj;
        return newObj;
    }
}

On a separate note it appears that imports have some node module resolution. The reason I say some is because you can’t use require within a CDN asset (it’s undefined).

@JayaKrishnaNamburu
Copy link
Author

I believe styled-components works because it does something similar to what I mentioned above in Option C, where the default property is interpolated within the package itself:

If the package is dealing the interop issue by itself. next dev still fails when i remove the alias. So, it's little confusing if the issue comes from the webpack or next. Ideally it should be the same behaviour for styled-components in both the setups right. If the package tries to solve it 🤔

@mattcarlotta
Copy link
Contributor

mattcarlotta commented Dec 13, 2021

I believe styled-components works because it does something similar to what I mentioned above in Option C, where the default property is interpolated within the package itself:

If the package is dealing the interop issue by itself. next dev still fails when i remove the alias. So, it's little confusing if the issue comes from the webpack or next. Ideally it should be the same behaviour for styled-components in both the setups right. If the package tries to solve it 🤔

This appears to have more complexity than I thought. In short, the styled function does not exist when rendered on the server. This is why you need to add the Webpack alias within the next.config.js file. On that note, even though it works with the alias, I have a feeling that you may run into issues where the styles generated on the server may not match the client (this is why styles are collected within the _document.js page for the official example).

Your codesandbox example above also appears to be rendered clientside only (which is why you’re not running into the same issues)

@JayaKrishnaNamburu
Copy link
Author

Looks like this opens door for new discussion, like does dynamic remote modules support SSR from urlImports. Or at the moment, purely client side. But feels good to see making steps towards ESM CDN's 😄

@jankaifer jankaifer self-assigned this Dec 1, 2022
@jankaifer jankaifer added the please verify canary The issue should be verified against next@canary. It will be closed after 30 days of inactivity label Dec 1, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Dec 1, 2022

Please verify that your issue can be recreated with next@canary.

Why was this issue marked with the please verify canary label?

We noticed the provided reproduction was using an older version of Next.js, instead of canary.

The canary version of Next.js ships daily and includes all features and fixes that have not been released to the stable version yet. You can think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces by running npm install next@canary and test it in your project, using your reproduction steps.

If the issue does not reproduce with the canary version, then it has already been fixed and this issue can be closed.

How can I quickly verify if my issue has been fixed in canary?

The safest way is to install next@canary in your project and test it, but you can also search through closed Next.js issues for duplicates or check the Next.js releases.

My issue has been open for a long time, why do I need to verify canary now?

Next.js does not backport bug fixes to older versions of Next.js. Instead, we are trying to introduce only a minimal amount of breaking changes between major releases.

What happens if I don't verify against the canary version of Next.js?

An issue with the please verify canary that receives no meaningful activity (e.g. new comments that acknowledge verification against canary) will be automatically closed and locked after 30 days.

If your issue has not been resolved in that time and it has been closed/locked, please open a new issue, with the required reproduction, using next@canary.

I did not open this issue, but it is relevant to me, what can I do to help?

Anyone experiencing the same issue is welcome to provide a minimal reproduction following the above steps. Furthermore, you can upvote the issue using the 👍 reaction on the topmost comment (please do not comment "I have the same issue" without repro steps). Then, we can sort issues by votes to prioritize.

I think my reproduction is good enough, why aren't you looking into it quicker?

We look into every Next.js issue and constantly monitor open issues for new comments.

However, sometimes we might miss one or two due to the popularity/high traffic of the repository. We apologize, and kindly ask you to refrain from tagging core maintainers, as that will usually not result in increased priority.

Upvoting issues to show your interest will help us prioritize and address them as quickly as possible. That said, every issue is important to us, and if an issue gets closed by accident, we encourage you to open a new one linking to the old issue and we will look into it.

Useful Resources

@balazsorban44
Copy link
Member

This issue has been automatically closed because it wasn't verified against next@canary. If you think it was closed by accident, please leave a comment. If you are running into a similar issue, please open a new issue with a reproduction. Thank you.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 2, 2023

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 2, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Metadata Related to Next.js' Metadata API. please verify canary The issue should be verified against next@canary. It will be closed after 30 days of inactivity
Projects
None yet
Development

No branches or pull requests

4 participants