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

[core] Add exports field to packages #41596

Closed
wants to merge 23 commits into from

Conversation

DiegoAndai
Copy link
Member

@DiegoAndai DiegoAndai commented Mar 21, 2024

Add exports field add .mjs extension to ESM files on the packages that use the common pipeline build.mjs/copyFiles.mjs except for:

Besides that, fix/adapt other configurations and imports to this new structure.

@DiegoAndai DiegoAndai added the core Infrastructure work going on behind the scenes label Mar 21, 2024
@DiegoAndai DiegoAndai self-assigned this Mar 21, 2024
@mui-bot
Copy link

mui-bot commented Mar 21, 2024

Netlify deploy preview

https://deploy-preview-41596--material-ui.netlify.app/

@mui/joy/MenuList: parsed: +0.90% , gzip: +1.02%
@mui/joy/Chip: parsed: +0.98% , gzip: +1.16%
RadioGroup: parsed: +1.32% , gzip: +1.45%
@mui/joy/ListSubheader: parsed: +1.17% , gzip: +1.13%
@mui/joy/RadioGroup: parsed: +1.16% , gzip: +1.14%
@mui/joy/Menu: parsed: +0.59% , gzip: +0.80%
@mui/joy/Drawer: parsed: +0.81% , gzip: +0.96%
Dialog: parsed: +0.89% , gzip: +0.98%
@mui/joy/Tooltip: parsed: +0.73% , gzip: +0.84%
@mui/joy/MenuItem: parsed: +1.00% , gzip: +0.99%
@mui/joy/Accordion: parsed: +1.00% , gzip: +1.22%
Rating: parsed: +1.12% , gzip: +1.15%
@mui/joy/Option: parsed: +0.98% , gzip: +1.03%
@mui/joy/FormControl: parsed: +1.07% , gzip: +1.22%
@mui/joy/ModalDialog: parsed: +0.91% , gzip: +0.90%
Tooltip: parsed: +0.76% , gzip: +0.70%
@mui/joy/Tab: parsed: +0.97% , gzip: +0.95%
@mui/joy/TabPanel: parsed: +1.08% , gzip: +1.17%
Hidden: parsed: +1.29% , gzip: +1.24%
@mui/joy/Checkbox: parsed: +0.91% , gzip: +0.94%
and 23 more changes

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against fb7a4ff

scripts/copyFilesUtils.mjs Outdated Show resolved Hide resolved
packages/mui-types/package.json Outdated Show resolved Hide resolved
type: 'array',
default: [],
})
.option('skipExportsField', {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be a temporal flag until we close #35233, which we're doing next. Depending on how the icons package is handled it might or might not be necessary.

packages/mui-core-downloads-tracker/package.json Outdated Show resolved Hide resolved
@DiegoAndai DiegoAndai marked this pull request as ready for review March 22, 2024 13:27
@DiegoAndai DiegoAndai changed the title [core] Add exports field to public packages [core] Add exports field to packages Mar 22, 2024
@@ -103,6 +103,10 @@ module.exports = function getBabelConfig(api) {
]);
}

if (process.env.MUI_ADD_IMPORT_EXTENSIONS === 'true') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not always add the extensions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regressions and rollup (umd) builds use this config file as well, and those break if we add the extensions.

When we remove the umd build, I could test if the issue with the regressions build is fixable, and we can remove this check. Does that make sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally al tests use builds that are as close to the real build as possible

Copy link
Member Author

@DiegoAndai DiegoAndai Mar 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, I'll look into adapting the regressions builds as well 👍🏼

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Janpot, I investigated the issue a bit. The regressions build uses babel.config.js to compile the packages as well as other files, like the docs data files and the regressions tests files. It also points to the src of the packages instead of the build (see the regressions webpack config and the base webpack config).

I was able to use the packages builds instead of the src code by doing

diff --git a/test/regressions/webpack.config.js b/test/regressions/webpack.config.js
index b4472eedd1..c9d02bb705 100644
--- a/test/regressions/webpack.config.js
+++ b/test/regressions/webpack.config.js
@@ -64,6 +64,20 @@ module.exports = {
       // Exclude polyfill and treat 'zlib' as an empty module since it is not required. next -> gzip-size relies on it.
       zlib: false,
     },
+    alias: {
+      ...webpackBaseConfig.resolve.alias,
+      '@mui/material': path.resolve(__dirname, '../../packages/mui-material/build'),
+      '@mui/icons-material': path.resolve(__dirname, '../../packages/mui-icons-material/build/esm'),
+      '@mui/lab': path.resolve(__dirname, '../../packages/mui-lab/build'),
+      '@mui/styled-engine': path.resolve(__dirname, '../../packages/mui-styled-engine/build'),
+      '@mui/styled-engine-sc': path.resolve(__dirname, '../../packages/mui-styled-engine-sc/build'),
+      '@mui/styles': path.resolve(__dirname, '../../packages/mui-styles/build'),
+      '@mui/system': path.resolve(__dirname, '../../packages/mui-system/build'),
+      '@mui/private-theming': path.resolve(__dirname, '../../packages/mui-private-theming/build'),
+      '@mui/base': path.resolve(__dirname, '../../packages/mui-base/build'),
+      '@mui/utils': path.resolve(__dirname, '../../packages/mui-utils/build'),
+      '@mui/joy': path.resolve(__dirname, '../../packages/mui-joy/build'),
+    },
   },

But even after doing that I still had to filter out some files and not add the file extensions to them:

diff --git a/babel.config.js b/babel.config.js
index 12d09a03c2..2d3ba4a891 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -103,10 +103,6 @@ module.exports = function getBabelConfig(api) {
     ]);
   }
 
-  if (process.env.MUI_ADD_IMPORT_EXTENSIONS === 'true') {
-    plugins.push(['babel-plugin-add-import-extension', { extension: useESModules ? 'mjs' : 'js' }]);
-  }
-
   return {
     assumptions: {
       noDocumentAll: true,
@@ -119,6 +115,12 @@ module.exports = function getBabelConfig(api) {
         exclude: /\.test\.(js|ts|tsx)$/,
         plugins: ['@babel/plugin-transform-react-constant-elements'],
       },
+      {
+        exclude: /(\/test\/regressions|packages\/mui-docs|docs)/,
+        plugins: [
+          ['babel-plugin-add-import-extension', { extension: useESModules ? 'mjs' : 'js' }],
+        ],
+      },
     ],
     env: {
       coverage: {

This is because the files in those folders do not have extensions, as we're not adding them. We could add them, but I think this is outside of this PR's scope.

In conclusion, I would do

  1. Maintain the MUI_ADD_IMPORT_EXTENSIONS flag for this PR, adding the extensions only when building using build.mjs
  2. Remove the UMD build in a separate PR
  3. Create an issue to use the package builds instead of src for the regression tests. In that issue's PR, we could revisit removing MUI_ADD_IMPORT_EXTENSIONS and modifying the regression infrastructure accordingly.

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok for me

@DiegoAndai DiegoAndai requested a review from Janpot March 22, 2024 19:13
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Janpot @cherniavskii I'm wondering how these changes will impact X's script: https://github.com/mui/mui-x/blob/next/scripts/copyFiles.mjs.

  • We'll have to do createPackageFile(true) to skip the exports field
  • For the module field .js => .mjs change, should I add a flag as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, X will have to adopt these changes at well, but likely not before the next major. in the meantime I think we'll need a compatibility mode indeed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will implement it.

@DiegoAndai
Copy link
Member Author

Hey @Janpot! A couple of things:


I added instructions for possible breaking changes of custom configurations:

Is there anything else we should add to that?


I'm wondering, do we need to add the modern build to the exports field?:

"exports": {
    "./modern": {
        "types": "./modern/index.d.ts",
        "default": "./modern/index.mjs",
    },
    "./modern/*": {
        "types": "./modern/*/index.d.ts",
        "default": "./modern/*/index.mjs",
    },
    ".": {
        "types": "./index.d.ts",
        "import": "./index.mjs",
        "default": "./node/index.js"
    },
    "./*": {
        "types": "./*/index.d.ts",
        "import": "./*/index.mjs",
        "default": "./node/*/index.js"
    }
}

To support https://next.mui.com/material-ui/guides/minimizing-bundle-size/#modern-bundle?

We won't need it for the legacy build as that's to be removed: #40958

@Janpot
Copy link
Member

Janpot commented Mar 27, 2024

Is there anything else we should add to that?

I don't think so, this seems clear to me.

I'm wondering, do we need to add the modern build to the exports field?:

If we want to keep supporting the modern bundle it we will likely need to do that. We can add a modern field? Not sure whether there already is a consensus around a field. In any case this will be a breaking change for users that were aliasing in their bundler to get to the modern version. We will need to update how they'd have to configure their bundlers. Modern bundlers have settings that allow you to give priority to a custom exports field:

This will also need to go in the migration guide then

If instead we want to keep supporting the modern bundle through aliasing we will have to add exports for them in the package.json

"exports": {
  "./modern": {
    ...
  },
  "./modern/Button": {
    ...
  }
} 

But I'd avoid that as per https://next.mui.com/material-ui/guides/minimizing-bundle-size/#how-to-use-custom-bundles

@DiegoAndai
Copy link
Member Author

DiegoAndai commented Mar 27, 2024

So, if I understand correctly, the options are:

1. Add a modern condition inside exports:

"exports": {
    ".": {
        "types": "./index.d.ts",
        "modern": "./modern/index.mjs"
        "import": "./index.mjs",
        "default": "./node/index.js"
    },
    "./*": {
        "types": "./*/index.d.ts",
        "modern": "./modern/*/index.mjs",
        "import": "./*/index.mjs",
        "default": "./node/*/index.js"
    }
}

2. Add ./modern and ./modern/* patterns inside exports:

"exports": {
    "./modern": {
        "types": "./modern/index.d.ts",
        "default": "./modern/index.mjs",
    },
    "./modern/*": {
        "types": "./modern/*/index.d.ts",
        "default": "./modern/*/index.mjs",
    },
    ".": {
        "types": "./index.d.ts",
        "import": "./index.mjs",
        "default": "./node/index.js"
    },
    "./*": {
        "types": "./*/index.d.ts",
        "import": "./*/index.mjs",
        "default": "./node/*/index.js"
    }
}
  • Pros:
    • No breaking changes
  • Cons:
    • Exposes ./modern

I would opt for 1., I don't think the breaking change is too bad. If we had the exports field back when the modern build was implemented, I would guess 1. is how it would've been implemented to avoid exposing /modern.

Copy link
Member

@Janpot Janpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverting my approval to avoid accidental merge before issues have been solved, mainly:

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Mar 28, 2024
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Apr 10, 2024
@DiegoAndai
Copy link
Member Author

@Janpot, this is ready for review once more 😊

  • For the compatibility mode, I refactored to use the process.env.MUI_SKIP_CORE_EXPORTS_FORMAT flag, which allows to disable all changes with a single flag (example) as well as more granular control (example). This should allow the X team to adopt the changes gradually.
  • For the modern build, I went with option 1. I added a section to the migration guide and updated the instructions as well. It's working as expected on Vite. I will also test on the other bundlers before merging. One issue with this approach is that it's a project-wide config and not per-dependency, so if another package uses the modern condition name, there might be a conflict, but I don't think it's too probable. Let me know if we should use option 2 instead.

@DiegoAndai DiegoAndai requested a review from Janpot April 15, 2024 16:49
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Apr 16, 2024
@Janpot
Copy link
Member

Janpot commented Apr 16, 2024

To be clear, the compatibility mode is an opt-out, not an opt-in, right? Would it be hard to make this an opt-in instead? That way we loosely couple the migration of X to this system from the merge of this PR.

Otherwise this PR seems good.

On the changes of the modern bundle I'd like to put this to attention of @michaldudak and @mnajdova to be aware of this and a final blessing.

@michaldudak
Copy link
Member

One concern that I have is that the "modern" condition may be used by another package as well, and since we can't control conditions per import, configuring a bundler to use "modern" will change imports in both MUI and 3rd party packages. Having the condition more specific (like "mui-modern") could reduce the risk of this problem.

@DiegoAndai
Copy link
Member Author

To be clear, the compatibility mode is an opt-out, not an opt-in, right? Would it be hard to make this an opt-in instead? That way we loosely couple the migration of X to this system from the merge of this PR.

Exactly. We can make it opt-in 👍🏼 I think it will be safer. I'll implement it.

Having the condition more specific (like "mui-modern") could reduce the risk of this problem.

I'll implement this as well.

I'll let you know when this changes are ready for review.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Apr 16, 2024
@DiegoAndai
Copy link
Member Author

@Janpot ready for review 😊

@DiegoAndai
Copy link
Member Author

@samuelsycamore I added you for copy review of the migration guide and updated bundle instructions.

Copy link
Member

@Janpot Janpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excelent 👌

@DiegoAndai
Copy link
Member Author

I'm doing some final testing. Please don't merge yet.

Copy link
Member

@Janpot Janpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per Slack DMs, revoking my review again until we've verified that this works on X/base/Toolpad


Read more about the `exports` field in the [Node.js documentation](https://nodejs.org/api/packages.html#exports).

This change limits the exported modules to the root import and one level deep imports.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This change limits the exported modules to the root import and one level deep imports.
This change limits the exported modules to root imports and those that are one level deep.

Read more about the `exports` field in the [Node.js documentation](https://nodejs.org/api/packages.html#exports).

This change limits the exported modules to the root import and one level deep imports.
If you were importing from deeper levels, you will need to update your imports:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you were importing from deeper levels, you will need to update your imports:
If you were previously importing from deeper levels, you must update your imports as shown below:

```

You might have to update your bundler configuration to support the new structure.
Following are some common use cases that require changes:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Following are some common use cases that require changes:
Here are some common use cases that require changes:


#### Importing CJS

If you were importing from `/node` as a workaround, this is no longer necessary as the `exports` field maps CJS to the correct files.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you were importing from `/node` as a workaround, this is no longer necessary as the `exports` field maps CJS to the correct files.
If you were previously importing from `/node` as a workaround, this is no longer necessary because the `exports` field maps CJS to the correct files.

#### Using the modern bundle

The way the modern bundle should be imported has changed.
Previously, you would alias `@mui/material` to `@mui/material/modern` in your bundler configuration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Previously, you would alias `@mui/material` to `@mui/material/modern` in your bundler configuration.
Previously you would alias `@mui/material` to `@mui/material/modern` in your bundler configuration.


The ESM code, previously under the `esm/` build, has been moved to the root of the package.
The CommonJS code, previously on the root, has been moved to the `node/` build.
The `exports` field has been added to the `@mui/system/package.json` file to improve the ESM and CJS builds split:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `exports` field has been added to the `@mui/system/package.json` file to improve the ESM and CJS builds split:
The `exports` field has been added to the `@mui/system/package.json` file to improve the split between ESM and CJS builds:


### Added exports field to package.json

The `exports` field has been added to the `@mui/material/package.json` file to improve the ESM and CJS builds split:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `exports` field has been added to the `@mui/material/package.json` file to improve the ESM and CJS builds split:
The `exports` field has been added to the `@mui/material/package.json` file to improve the split between ESM and CJS builds:


Read more about the `exports` field in the [Node.js documentation](https://nodejs.org/api/packages.html#exports).

This change limits the exported modules to the root import and one level deep imports.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as the suggestions in the other doc above


:::
The modern bundle targets the latest released versions of evergreen browsers (Chrome, Firefox, Safari, Edge).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The modern bundle targets the latest released versions of evergreen browsers (Chrome, Firefox, Safari, Edge).
The modern bundle targets the latest released versions of the most popular browsers: Chrome, Firefox, Safari, and Edge.


:::
The modern bundle targets the latest released versions of evergreen browsers (Chrome, Firefox, Safari, Edge).
This can be used to make separate bundles targeting different browsers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This can be used to make separate bundles targeting different browsers.
This can be used to create separate bundles that target different browsers.

@DiegoAndai
Copy link
Member Author

Closing this PR as per: #30671 (comment).

@DiegoAndai DiegoAndai closed this May 24, 2024

await createModulePackages({ from: srcPath, to: buildPath });
await createModulePackages({ from: srcPath, to: buildPath, exportFormat });
Copy link
Member

@Janpot Janpot Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DiegoAndai When you have a moment, you should try whether our problem with @mui/material-nextjs goes away if we remove this line.

Suggested change
await createModulePackages({ from: srcPath, to: buildPath, exportFormat });
// await createModulePackages({ from: srcPath, to: buildPath, exportFormat });

I had a recent experience in Toolpad where ESM/CJS got mixed up in _document.js which got fixed after I removed the internal package.json files. They become obsolete once we move to the "exports" field and keeping them seems to confuse some bundlers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change core Infrastructure work going on behind the scenes PR: out-of-date The pull request has merge conflicts and can't be merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants