I have a React application with a page importing a component from another file.
// code from IconPage.tsx
import {AccountBalanceIcon} from './icons';
<AccountBalanceIcon />
// Code from ./icons.ts
export { default as AccountBalanceIcon } from "@material-ui/icons/AccountBalance";
This works fine when the exporting file is a typescript (.ts
) file, however when I rename the file to .js
, I get significantly different behavior.
// Code from ./icons.js
export { default as AccountBalanceIcon } from "@material-ui/icons/AccountBalance";
This errors during webpack's build step
Module not found: Error: Can't resolve '@material-ui/icons/AccountBalance' in '.../src/pages'
Did you mean 'AccountBalance.js'?
BREAKING CHANGE: The request '@material-ui/icons/AccountBalance' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request.
Yes, I'm using "type": "module"
in my package.json
, so I refactor the code so it compiles by adding .js
extension.
// Code from ./icons.js
export { default as AccountBalanceIcon } from "@material-ui/icons/AccountBalance.js";
Once this is done, webpack builds, but I get the familiar React error in the browser console when you mess up your import statement.
react-dom.development.js:25058 Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Check the render method of `IconPage`.
at createFiberFromTypeAndProps (react-dom.development.js:25058:1)
at createFiberFromElement (react-dom.development.js:25086:1)
at createChild (react-dom.development.js:13446:1)
Renaming the file back to .ts
(and even keeping the AccountBalance.js
in place), resolves the runtime error.
Analysis
The reason for the discrepancy is shown in the build code.
As a JavaScript source file, the resulting code webpack produces is as follows.
__webpack_require__.d(__webpack_exports__, {
"AccountBalanceIcon": () =>
(_material_ui_icons_AccountBalance_js__WEBPACK_IMPORTED_MODULE_0__)
});
And the typescript output, looks like this.
__webpack_require__.d(__webpack_exports__, {
"AccountBalanceIcon": () =>
(_material_ui_icons_AccountBalance_js__WEBPACK_IMPORTED_MODULE_0__["default"])
});
Note, the typescript source references the imported module's .default
property, while the JavaScript source code refers to the module itself.
tsc --showConfig
{
"compilerOptions": {
"target": "es2017",
"jsx": "react",
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"allowJs": true,
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"./src/**/*"
],
"exclude": [
"./test/**/*"
]
}
webpack.config.js
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
const pathResolve = (path) => new URL(path, import.meta.url).pathname;
export default {
mode: process.env.NODE_ENV === "production" ? "production" : "development",
devtool: "inline-source-map",
entry: "./src/index.tsx",
output: {
path: pathResolve("dist"),
filename: "bundle.js",
publicPath: '/',
},
devServer: {
historyApiFallback: true,
hot: true,
port: 3000,
},
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
module: {
rules: [
{ test: /\.tsx?$/, exclude: /node_modules/, loader: "ts-loader"},
{ test: /\.js$/, use: ["source-map-loader"], enforce: "pre" },
{ test: /\.css$/, use: ['style-loader', 'css-loader']},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins:[
new HtmlWebpackPlugin({
template: pathResolve("./public/index.html"),
filename: "index.html"
}),
new MiniCssExtractPlugin({filename: "app.css"}),
],
ignoreWarnings: [/Failed to parse source map/],
};
I expect my problem is actually related to one of these config files.
Question
How can I get the JavaScript file to export the Material-UI component cleanly for JS and TS projects to consume the export?
I ask because the icons.ts
file in this example is actually a simplification to show the problem. The code for the icons is part of a larger component library which is packaged and distributed as transpiled JS files.