➜ mkdir module-federation-ts-example

➜ cd module-federation-ts-example

➜ pnpm init

➜ corepack use [email protected]

➜ code .
# module-federation-basic-example/pnpm-workspace.yaml

packages:
  - "apps/*"

2개의 워크스페이스 생성

➜ mkdir apps

➜ cd apps

➜ pnpm create mf-app # (main-app, 3000)

➜ pnpm create mf-app # (component-app, 3001)

➜ cd ..

➜ pnpm i
{
  "name": "main-app",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "build:start": "cd dist && PORT=3000 npx serve",
    "start": "webpack serve --open --mode development",
    "start:live": "webpack serve --open --mode development --live-reload --hot"
  },
  "license": "MIT",
  "author": {
    "name": "Jack Herrington",
    "email": "[email protected]"
  },
  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/plugin-transform-runtime": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.10.4",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "autoprefixer": "^10.1.0",
    "babel-loader": "^8.2.2",
    "css-loader": "^6.3.0",
    "html-webpack-plugin": "^5.3.2",
    "postcss": "^8.2.1",
    "postcss-loader": "^4.1.0",
    "style-loader": "^3.3.0",
    "typescript": "^4.5.2",
    "webpack": "^5.57.1",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.3.1"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}
{
  "name": "component-app",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "build:start": "cd dist && PORT=3001 npx serve",
    "start": "webpack serve --open --mode development",
    "start:live": "webpack serve --open --mode development --live-reload --hot"
  },
  "license": "MIT",
  "author": {
    "name": "Jack Herrington",
    "email": "[email protected]"
  },
  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/plugin-transform-runtime": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.10.4",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "autoprefixer": "^10.1.0",
    "babel-loader": "^8.2.2",
    "css-loader": "^6.3.0",
    "html-webpack-plugin": "^5.3.2",
    "postcss": "^8.2.1",
    "postcss-loader": "^4.1.0",
    "style-loader": "^3.3.0",
    "typescript": "^4.5.2",
    "webpack": "^5.57.1",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.3.1"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}
➜ pnpm i

component-app 워크스페이스

// module-federation-ts-example/apps/component-app/src/components/Button.tsx

import React, { type PropsWithChildren } from "react";

const styleMapping = {
  primary: {
    marginLeft: "10px",
    color: "#fff",
    backgroundColor: "#409eff",
    borderColor: "#409eff",
    padding: "12px 20px",
    fontSize: "14px",
    borderRadius: "4px",
    outline: "none",
    border: "1px solid #dcdfe6",
    cursor: "pointer",
  },
  warning: {
    marginLeft: "10px",
    color: "#fff",
    backgroundColor: "#e6a23c",
    borderColor: "#e6a23c",
    padding: "12px 20px",
    fontSize: "14px",
    borderRadius: "4px",
    outline: "none",
    border: "1px solid #dcdfe6",
    cursor: "pointer",
  },
};

export type ButtonProps = PropsWithChildren<{
  type?: "primary" | "warning";
  onClick?: React.MouseEventHandler<HTMLButtonElement>;
}>;

const Button: React.FC<ButtonProps> = ({ type, children, onClick }) => {
  const buttonType = type === "warning" ? "warning" : "primary";

  return <button style={styleMapping[buttonType]} onClick={onClick}>{children}</button>;
};

export default Button;
// module-federation-ts-example/apps/component-app/src/App.tsx

import React from "react";
import ReactDOM from "react-dom/client";

import "./index.css";

import Button from "./components/Button";

const App = () => (
  <div className="container">
    <div>Name: component-app</div>
    <div>Framework: react</div>
    <div>Language: JavaScript</div>
    <div>CSS: Empty CSS</div>
    <Button>Sample</Button>
  </div>
);
ReactDOM.createRoot(document.getElementById("app")).render(<App />);
// module-federation-ts-example/apps/component-app/webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
  output: {
    publicPath: "<http://localhost:3001/>",
  },

  resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
  },

  devServer: {
    port: 3001,
    historyApiFallback: true,
  },

  module: {
    rules: [
      {
        test: /\\.m?js/,
        type: "javascript/auto",
        resolve: {
          fullySpecified: false,
        },
      },
      {
        test: /\\.(css|s[ac]ss)$/i,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },

  plugins: [
    new ModuleFederationPlugin({
      name: "component_app",
      filename: "remoteEntry.js",
      remotes: {},
      exposes: {
        "./Button": "./src/components/Button",
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./src/index.html",
    }),
  ],
});
➜ pnpm --filter component-app start:live

http://localhost:3001/

http://localhost:3001/remoteEntry.js

main-app 워크스페이스

➜ pnpm --filter main-app add react-error-boundary
// module-federation-ts-example/apps/main-app/src/App.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import { ErrorBoundary } from "react-error-boundary";

import "./index.css";

// @ts-ignore
const Button = React.lazy(() => import("component_app/Button"));

const App = () => (
  <div className="container">
    <div>Name: main-app</div>
    <div>Framework: react</div>
    <div>Language: TypeScript</div>
    <div>CSS: Empty CSS</div>
    <ErrorBoundary fallback={<div>Error</div>}>
      <React.Suspense fallback={<div>Loading...</div>}>
        <Button
          onClick={() => {
            console.log("Clicked!!");
          }}
        >
          Primary
        </Button>
      </React.Suspense>
    </ErrorBoundary>
    <ErrorBoundary fallback={<div>Error</div>}>
      <React.Suspense fallback={<div>Loading...</div>}>
        <Button type="warning">Warning</Button>
      </React.Suspense>
    </ErrorBoundary>
  </div>
);
ReactDOM.createRoot(document.getElementById("app")!).render(<App />);
// module-federation-basic-example/apps/main-app/webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
  output: {
    publicPath: "<http://localhost:3000/>",
  },

  resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
  },

  devServer: {
    port: 3000,
    historyApiFallback: true,
  },

  module: {
    rules: [
      {
        test: /\\.m?js/,
        type: "javascript/auto",
        resolve: {
          fullySpecified: false,
        },
      },
      {
        test: /\\.(css|s[ac]ss)$/i,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },

  plugins: [
    new ModuleFederationPlugin({
      name: "main_app",
      filename: "remoteEntry.js",
      remotes: {
        component_app: "component_app@<http://localhost:3001/remoteEntry.js>",
      },
      exposes: {},
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./src/index.html",
    }),
  ],
});
➜ pnpm --filter main-app start:live

http://localhost:3000/