diff --git a/README.md b/README.md index 956dd8a..ffc140c 100644 --- a/README.md +++ b/README.md @@ -1,631 +1,190 @@ -# What is Scalprum? +# Scalprum -Scalprum is a tool for building dynamic frontend UIs from a variety of different sources. Thanks to Scalprum’s dynamic nature, you can pick and choose different components that you want to pull into your UI without having to worry about rebuilding your UI each time you pull in a change. Scalprum has been built with configurability in mind - you can manage different outputs all from one configuration file, without the need to fill your code with conditionals. +**A powerful micro-frontend framework for React applications** -Scalprum is a JavaScript micro frontend framework. It leverages [webpack](https://webpack.js.org/) 5 and its [module federation](https://webpack.js.org/concepts/module-federation/#root) features to create fast and scaleable micro frontend environments. +Scalprum is a JavaScript micro-frontend framework that enables you to build dynamic, scalable applications by composing components from multiple sources. Built on top of [Module Federation](https://module-federation.io/), Scalprum provides a developer-friendly abstraction for managing federated modules with advanced features like Remote Hooks and runtime plugin systems. -# Roadmap +## Key Features -See our [roadmap](./ROADMAP.md) to v1 to get of a future fo Scalprum. +- 🧩 **Dynamic Module Loading** - Load React components from remote sources at runtime +- 🎣 **Remote Hooks** - Share React hooks across micro-frontends seamlessly +- ⚑ **High Performance** - Built on Module Federation for optimal bundle sharing +- πŸ”§ **Build Tool Agnostic** - Compatible with Webpack 5, Rspack, and Module Federation Runtime +- 🎯 **Type Safe** - Full TypeScript support with comprehensive type definitions +- πŸ”Œ **Plugin Architecture** - Extensible system for building plugin-based applications +## Quick Start -# Getting started - -### Prerequisites - -- Ensure that you have [Node.js](https://nodejs.org/en/download/) installed. -- Environment using webpack 5 to build the final output -- Using React frontend library - -### Procedure - -#### Demo environment setup - -The following steps outline an example webpack development setup for demo purposes. -You can adjust the steps to suit your own development requirements. - -1. Create a working directory for the project: -```sh -mkdir scalprum-demo && cd scalprum-demo -``` -2. Use the following command to initialise a node. -```sh -npm init -``` -3. Optional: Depending on your needs, you might want to create a Git repository: -```sh -git init -``` -4. Create a webpack project to set up the development dependencies: -```sh -npm i --save-dev webpack webpack-cli webpack-dev-server swc-loader -``` -5. Install swc-loader so that the webpack can understand react: -```sh -npm i --save-dev swc-loader @swc/core -``` -6. Generate a default webpack configuration, and enter 'y' to any options you want to include: -```sh -npx webpack init -``` -7. Edit the `tsconfig.json` file to match the following configuration: -```js - -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "noImplicitAny": true, - "module": "ESNext", - "target": "ESNext", - "allowJs": true, - "moduleResolution": "node", - "jsx": "react", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - }, - "include": ["src/**/*tsx", "src/**/*ts"] -} - -``` -8. Install Scalprum and its dependencies: -```sh -npm i react react-dom @scalprum/core @scalprum/react-core -``` -9. Add a type definition for react: -```sh -npm i --save-dev @types/react-dom @types/react +```bash +npm install @scalprum/core @scalprum/react-core ``` -10. Edit the `index.html` file to add the root element also to the html: -```htmlembedded= - - - - - Webpack App - - -
- - -``` - -11. Edit the `webpack.config.js` file to match the following configuration: -```js -// Generated using webpack-cli https://github.com/webpack/webpack-cli - -const path = require("path"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const { ModuleFederationPlugin } = require('webpack').container; - -const isProduction = process.env.NODE_ENV == "production"; -const stylesHandler = MiniCssExtractPlugin.loader; +```jsx +import { ScalprumProvider, ScalprumComponent } from '@scalprum/react-core'; const config = { - entry: "./src/index.ts", - output: { - path: path.resolve(__dirname, "dist"), - }, - devServer: { - open: true, - host: "localhost", - }, - plugins: [ - new HtmlWebpackPlugin({ - template: "index.html", - }), - new MiniCssExtractPlugin(), - new ModuleFederationPlugin({ - name: 'shell', - filename: isProduction ? 'shell-entry.[contenthash].js' : 'shell-entry.js', - shared: [ - { - react: { - requiredVersion: '*', - singleton: true, - }, - 'react-dom': { - requiredVersion: '*', - singleton: true, - }, - '@scalprum/react-core': { singleton: true, requiredVersion: '*' }, - '@openshift/dynamic-plugin-sdk': { singleton: true, requiredVersion: '*' }, - }, - ], - }), - ], - module: { - rules: [ - { - test: /\.(ts|tsx)$/i, - use: { - loader: 'swc-loader', - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true - } - } - } - }, - exclude: ["/node_modules/"], - }, - { - test: /\.css$/i, - use: [stylesHandler, "css-loader"], - }, - { - test: /\.s[ac]ss$/i, - use: [stylesHandler, "css-loader", "sass-loader"], - }, - { - test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, - type: "asset", - }, - - // Add your rules for custom modules here - // Learn more about loaders from https://webpack.js.org/loaders/ - ], - }, - resolve: { - extensions: [".tsx", ".ts", ".jsx", ".js", "..."], - }, -}; - -module.exports = () => { - if (isProduction) { - config.mode = "production"; - } else { - config.mode = "development"; + myModule: { + name: 'myModule', + manifestLocation: '/path/to/plugin-manifest.json' } - return config; }; -``` -12. Change the `index.ts` file to match the following example: -```ts -import('./bootstrap.tsx') -``` -Create a `bootstrap.tsx` file with this code: - -```js - -import React from 'react'; -import { createRoot } from 'react-dom/client'; - -const App = () => { +function App() { return ( -
I am an app
- ) -} - -const domNode = document.getElementById('root'); -if(domNode) { - const root = createRoot(domNode); - root.render() + + + + ); } - ``` -If everything is set up, you can run `npm run serve` and view the example text that you entered in the `
` of `index.tsx`. +## Packages -### Scalprum setup +| Package | Description | +|---------|-------------| +| [`@scalprum/core`](./packages/core) | Framework-agnostic core for module federation | +| [`@scalprum/react-core`](./packages/react-core) | React bindings with hooks and components | +| [`@scalprum/build-utils`](./packages/build-utils) | Build tools and NX executors | +| [`@scalprum/react-test-utils`](./packages/react-test-utils) | Testing utilities for Scalprum apps | -To use Scalprum, we must create a **host application**. +## Documentation -A *host application* is the main provider that manages module loading, routing, data sharing, and related tasks. It functions as the manager of the micro-frontend. -The host application always loads first. -It can be any React application as long as it has some mandatory webpack configuration. +Documentation is organized within individual package directories, following monorepo best practices. This ensures package READMEs appear on npm automatically and documentation stays aligned with the code it documents. -1. To create a host application - a top-level application to manage the micro frontend, in the `scalprum-demo/src` directory, create a `ScalprumRoot.tsx` file, and edit to match the following configuration: -```jsx +- πŸ“š **[Getting Started Guide](./packages/react-core/docs/getting-started.md)** - Complete setup tutorial +- πŸ“¦ **Package Documentation**: + - [@scalprum/core](./packages/core/README.md) - Framework-agnostic core + - [@scalprum/react-core](./packages/react-core/README.md) - React bindings and hooks + - [@scalprum/build-utils](./packages/build-utils/README.md) - Build tools and NX executors + - [@scalprum/react-test-utils](./packages/react-test-utils/README.md) - Testing utilities +- 🎣 **Remote Hooks** - Share hooks across micro-frontends: + - [Overview](./packages/react-core/README.md#remote-hooks) + - [useRemoteHook Guide](./packages/react-core/docs/use-remote-hook.md) + - [useRemoteHookManager Guide](./packages/react-core/docs/use-remote-hook-manager.md) + - [RemoteHookProvider Reference](./packages/react-core/docs/remote-hook-provider.md) + - [Type Definitions](./packages/react-core/docs/remote-hook-types.md) -import React from 'react'; -import { AppsConfig } from '@scalprum/core' -import { ScalprumProvider, ScalprumComponent } from '@scalprum/react-core' +## Getting Started -const config: AppsConfig = { - remoteModule: { - name: 'remoteModule', - manifestLocation: 'http://localhost:8003/plugin-manifest.json' - } -} +### Prerequisites -const ScalprumRoot = () => { - return ( -
- - - -
- ) -} +- [Node.js](https://nodejs.org/en/download/) 16+ +- Build environment with Webpack 5, Rspack, or Module Federation Runtime support +- React 16.8+ (for hooks support) -export default ScalprumRoot; -``` +### Basic Setup -Let's take a look at what we set up in this example. +1. **Install Scalprum packages:** + ```bash + npm install @scalprum/core @scalprum/react-core + ``` +2. **Create a host application:** + ```jsx + import { ScalprumProvider, ScalprumComponent } from '@scalprum/react-core'; + const config = { + myModule: { + name: 'myModule', + manifestLocation: 'http://localhost:8003/plugin-manifest.json' + } + }; -| Component | Definition | -| -------- | -------- | -| **`AppsConfig module`** | The **`AppsConfig`** is a set of dynamic modules from '@scalprum/core'. This is framework-agnostic and will work as long as it is in Javascript. | -|**`@scalprum/core`** package| The **`@scalprum/core`** package is responsible for managing federated modules. It provides an abstraction on low-level API to make it more developer friendly alongside additional features. Behind the scenes, it uses our exposed webpack modules, **`@openshift/dynamic-plugin-sdk`**, to manage plugins.| -|**`ScalprumProvider`** module|The **`ScalprumProvider`** is the main root component. If you want to load modules, you must ensure they are enclosed in the `` components. `ScalprumProvider` has a mandatory `config` field. This informs `ScalprumProvider` of which remote modules are available to render. For the purpose of the demo, `const config: AppsConfig = {}`, we set up `remoteModule` to use in the next steps. The config requires a `name`, and a `manifestLocation`.| -|**`@scalprum/react-core`**|The **`@scalprum/react-core`** packages provide react core bindings for `@scalprum/core`.| -|**`ScalprumComponent`**|The **`ScalprumComponent`** is provided by the **`@scalprum/react-core`** packages. It requires two parameters: **`scope`**: the `name` that you set in the `ScalprumProvider`'s `config{}`. **`module`**: the actual module that you want to render. For the purposes of this demo, create `RemoteModuleComponent`. -| + function App() { + return ( + + + + ); + } + ``` +3. **Configure Module Federation in your bundler** -2. Edit the `index.tsx` file and make the following changes: +For a complete step-by-step tutorial including remote module setup, see our **[Getting Started Guide](./packages/react-core/docs/getting-started.md)**. -```jsx -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import ScalprumRoot from './ScalprumRoot'; +## API Reference -const App = () => { - return ( - - ) -} +### Core Concepts -const domNode = document.getElementById('root'); -if(domNode) { - const root = createRoot(domNode); - root.render() -} -``` - -3. In the `webpack.config.js` file, add the `moduleFederation` plugin to set up the environment for plugin applications to use the context of the host applications: +**Host Application**: The main application that manages module loading, routing, and data sharing. It loads first and provides the foundation for your micro-frontend architecture. -```jsx -// declare shared dependencies -const moduleFederationPlugin = new ModuleFederationPlugin({ - name: 'host', - filename: 'host.[contenthash].js', - shared: [ - { - '@openshift/dynamic-plugin-sdk': { - singleton: true, - requiredVersion: '*', - }, - }, - { - '@scalprum/react-core': { - singleton: true, - requiredVersion: '*', - }, - }, - { react: { singleton: true, requiredVersion: '*' } }, - { 'react-dom': { singleton: true, requiredVersion: '*'} }, - // any other packages you wish to share - ], -}); -``` +**Remote Modules**: Independent applications that can be loaded dynamically at runtime. They expose components that can be consumed by the host application. -Note that for demo purposes, the `requiredVersion` for dependencies in this example is set to `*`, but you can update this to a version that suits your requirements. +### Build Tool Compatibility -4. In the `webpack.config.js` file, add an entry also for the `moduleFederationPlugin` to the declared plugins: +Scalprum works with multiple build tools and module federation implementations: -```jsx - plugins: [ - new HtmlWebpackPlugin({ - template: "index.html", - }), - new MiniCssExtractPlugin(), - // scalprum required plugin - moduleFederationPlugin, - ], -``` -5. To create the remote module, in the `scalprum-demo` directory, create a **remoteModule** directory. -6. Install the following dependencies: -```shell -npm i --save-dev @openshift/dynamic-plugin-sdk-webpack -``` -7. In the **scalprum-demo/remoteModule** directory, create a `webpack.config.js` file. -8. You can copy the contents of `webpack.config.js` file from the previous steps. -9. In the `webpack.config.js` file, remove the `HtmlWebpackPlugin` because it breaks dynamic modules. -This is a known issue that is being actively worked on. -10. Remove the `moduleFederationPlugin` and replace with the `DynamicRemotePlugin`: -```jsx -// declare shared dependencies +- **Webpack 5**: Native Module Federation support +- **Rspack**: High-performance Rust-based bundler with Module Federation +- **Module Federation Runtime**: Framework-agnostic module federation for any bundler -const { DynamicRemotePlugin } = require('@openshift/dynamic-plugin-sdk-webpack'); +### Module Federation Configuration -const sharedModules = { - '@openshift/dynamic-plugin-sdk': { singleton: true }, +**Required shared dependencies** (must be marked as singletons): +```js +const shared = { '@scalprum/react-core': { singleton: true }, - react: { singleton: true }, - 'react-dom': { singleton: true }, + 'react': { singleton: true }, + 'react-dom': { singleton: true } }; - -const dynamicPlugin = new DynamicRemotePlugin({ - extensions: [], - sharedModules, - entryScriptfilename: 'remoteModule.[contenthash].js', - pluginMetadata: { - name: 'remoteModule', - version: '1.0.0', - exposedModules: { - RemoteModuleComponent: './remoteModule/src/RemoteModuleComponent.tsx', - }, - extensions: [], - }, -}); - ``` -11. In the `webpack.config.js` file, also replace the `moduleFederationPlugin` with the `dynamicPlugin` plugins: -```jsx - plugins: [ +### ScalprumProvider - new MiniCssExtractPlugin(), - // scalprum required plugin - dynamicPlugin - , - ], -``` -12. In the `webpack.config.js` file, also add a `publicPath`: -```jsx -const config = { - entry: "./src/index.tsx", - output: { - path: path.resolve(_dirname, "dist"), - publicPath:'http://localhost:8003' - }, -devServer: { - open: true, - host: "localhost", -}, - -``` +Root component that provides module loading context: -13. In your **scalprum-demo/remoteModule** directory, create a **src** directory. -14. In your **scalprum-demo/remoteModule/src** directory, create an `index.tsx` file. -15. In your **scalprum-demo/remoteModule/src** directory, create a `RemoteModuleComponent.tsx` file and add the following: ```jsx -import React from 'react'; - -const RemoteModuleComponent = () => { - return ( -
- I am a remote module component; -
- ) -} - -export default RemoteModuleComponent; - -``` - -16. In the `scalprum-demo/package.json` file, add the following entry and point to `remoteModule/webpack.config.js` as your configuration file: - -```json - -"build:plugin":"webpack --mode=production --node-env=production -c remoteModule/webpack.config.js" - -``` - - -To test if everything is set up correctly, enter the following command: - -```shell -npm run build:plugin -``` - -Normally, your modules would be served via your own content delivery network, but for demo purposes, you can serve it locally from the **dist** directory that is created when you run `npm run build:plugin`. - -```shell -cd scalprum-demo/remoteModule/dist/ && npx http serve . -p 8003 -``` - -If this action completes without error, you can view your plugin manifest at `http://localhost:8003/`. -You can also run the host application via `npm run serve` and check if the dynamic plugin was loaded correctly in the UI. - -### Reference information - - -***dependencies*** -```sh -npm i @openshift/dynamic-plugin-sdk @scalprum/core @scalprum/react-core -``` - -#### Webpack config - -TODO: Create host webpack plugin or extensible default webpack config - -```js -// webpack.config.js - -// require module federation plugin -const { ModuleFederationPlugin } = require('webpack').container; - -// declare shared dependencies -const moduleFederationPlugin = new ModuleFederationPlugin({ - name: 'host', - filename: 'host.[contenthash].js', - shared: [{ - // These packages has to be shared and has to be marked as singleton! - { '@openshift/dynamic-plugin-sdk', { singleton: true, }} - { '@scalprum/react-core': { singleton: true, } }, - { react: { singleton: true, } }, - { 'react-dom': { singleton: true } }, - // any other packages you wish to share - }] -}) - -module.exports = { - // regular react webpack config - plugins: [moduleFederationPlugin, ... /** other plugins you need */] -} -``` - -The host application requires an extra module federation plugin to be compatible with scalprum packages. - -The following modules have to be shared and marked as singletons: -- `@scalprum/react-core` -- `react` -- `react-dom` - -If your application requires additional shared/singleton packages (eg. `react-router-dom`) they can be added to the plugin configuration. - -#### ScalprumProvider - -The `ScalprumProvider` is a root React node that propagates necessary context to its children. It requires a configuration object that is referenced when a module is loaded. - -```JSX -import App from './App' +import { ScalprumProvider } from '@scalprum/react-core'; const config = { - testModuleOne: { - name: 'testModuleOne', // module name - manifestLocation: '/path/to/manifest/file.json' // metadata file with module entry script location and other information - }, - testModuleTwo: { - name: 'testModuleTwo', - manifestLocation: ... - } -} - -const HostRoot = () => { - return ( - - - - ) -} - -export default HostRoot -``` - -***config*** - -The config prop contains is a module registry. The config data is used to load and initialize modules at runtime. - -```TS -type Config = { - [name: string]: { - name: string; - manifestLocation: string; + myModule: { + name: 'myModule', + manifestLocation: '/path/to/plugin-manifest.json' } -} -``` - - -***api*** - -The `api` prop is an object that is available to all provider children nodes via `useScalprum` hook. It is a good place to store your global context (user, auth API, etc...). - -```JSX -const HostRoot = () => { - return ( - - - - ) -} - -import { useScalprum } from '@scalprum/react-core' - -const Plugin = () => { - const { api: { user } } = useScalprum() - return ( -
-

Hello {user.name}

-
- ) -} -``` - -#### ScalprumComponent - -The `ScalprumComponent` is a react binding that directly renders a module. The referenced module has to be a React component. The module also has to be present in the `ScalprumProvider` config. - -```JSX -import { ScalprumComponent } from '@scalprum/react-core'; - -const RemotelyLoadedComponent = () => { - - const anyProps = { - foo: 'bar' - } - - return ( - - ); -} +}; + + + ``` -***importName*** +### ScalprumComponent -The `importName` prop is a string that is used to reference the module. It is a name of the exported component from the module. Should be used if other than `default` export is required. +Component for rendering remote modules: ```jsx -// Remote module definition -export const NamedComponent = () => { - return ( -
-

Named component

-
- ) -} +import { ScalprumComponent } from '@scalprum/react-core'; -// Consumer - +} + ErrorComponent={ErrorFallback} + // Additional props passed to remote component + customProp="value" +/> ``` -One module can have multiple exports. -***fallback*** +## Contributing -Similar to [React.Suspense](https://beta.reactjs.org/reference/react/Suspense). This component will be loaded before the module is ready. +We welcome contributions to Scalprum! Whether it's bug reports, feature requests, or code contributions, your help makes this project better. -```jsx -}> -``` +### How to Contribute -***ErrorComponent*** +- **[Report bugs or request features](https://github.com/scalprum/scaffolding/issues)** - Open an issue on GitHub +- **[Join discussions](https://github.com/scalprum/scaffolding/discussions)** - Ask questions or share ideas +- **Submit pull requests** - Fix bugs or add new features +- **Improve documentation** - Help make our docs better +- **Share examples** - Contribute real-world usage examples -A node that is rendered if a module encountered a runtime error or failed to load. +For development setup and guidelines, check the individual package READMEs and explore the `examples/` directory for reference implementations. +## Links -```jsx -// error type cannot be strict and depends on type of error and on application -const ErrorComponent = ({ error, errorInfo }) => { - useEffect(() => { - // handle the error (report to logging service) - }, []) - return ( -
-

Error rendering component

-
- ) -} - - -}> -``` +- **[GitHub Repository](https://github.com/scalprum/scaffolding)** +- **[Issues](https://github.com/scalprum/scaffolding/issues)** +- **[Discussions](https://github.com/scalprum/scaffolding/discussions)** \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 4e01607..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,42 +0,0 @@ -# Scalprum roadmap to v1.0.0 - -The following items are planned to be added to the Scalprum project. There is no "date" for any of them, but they are sorted by their priority. By the end of the list, the priorities are a bit fuzzy. - -## Move the [@openshift/dynamic-plugin-sdk](https://github.com/openshift/dynamic-plugin-sdk) to the Scalprum project - -The `@openshift/dynamic-plugin-sdk` and the `@openshift/dynamic-plugin-sdk-webpack` are tightly coupled together. - -The `@openshift/*` packages provide low API to access the module federation features. Currently, Scalprum is more concerned with the developer facing API. - -Historically, Scalprum had its own module federation API, but because of the similarities with the SDK packages, and to save some time, it was decided to combine these two into a single project. - -Now that all projects have been migrated or are in the process of migration to the latest version of the `@openshift/dynamic-plugin-sdk` and no more breaking changes are planned, we can kick off the movement of the packages under a single umbrella. - -This move is purely formal. No changes to the current behavior are planned in this goal. - -## Adopting [@module-federation/*](https://github.com/module-federation/universe/tree/main) packages - -When Scalprum and the plugin SDK were created, the `@module-federation` packages did not exist. Now that the module federation concept has existed for some time and it matured, we can now look to other open-source packages to take over the low-level module federation APIs and focus on additional features, rather than maintaining the module federation APIs. - -The `@module-federation/*` packages have been out for some time and are in a good state for us to "give up" some of the code. - -An additional benefit of this move is compatibility with the rest of the community rather than carving our path. Creating "module federation" framework is not the long-term goal of this project. The project is focused purely on enabling micro frontends for React applications. And maybe one day even a framework. Not the build tooling that enables it. - -## Enabling SSR support - -Currently, Scalprum is not compatible with SSR. We have already made a working POC and we know it works well. - -## Looking beyond webpack - -As of now, Scalprum works only with webpack. We know that other build tools support these features. Namely `Vite` and more recently `Rspack`. - -Both of the above-mentioned options will be at minimum explored and tested. Both of the projects claim compatibility with webpack. That should make the migration easy and even potentially allow the mixing of the tooling in a single project. - -## Building proper documentation site - -Right now, the documentation is lacking. We realize that. As of recently, Scalprum has been used mostly in internal projects. That is starting to change and it means we have to provide better documentation for everybody. - -We see the above-mentioned items of this roadmap to be necessary for a proper v1 release. Once we are happy with the state of the other items on the roadmap, we will combine the currently very spread resources to create proper documentation. - - -## V1 Release diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 60d5daf..0000000 --- a/docs/README.md +++ /dev/null @@ -1,227 +0,0 @@ -# Scalprum Remote Hooks Documentation - -Remote Hooks is a powerful feature in Scalprum that allows you to load and execute React hooks from federated modules dynamically. This enables true micro-frontend architecture where not only components but also custom hooks can be shared across applications. - -## Overview - -Remote Hooks provide a way to: -- **Load hooks from federated modules** - Execute hooks from other applications -- **Dynamic hook management** - Add, remove, and control multiple hooks at runtime -- **Seamless integration** - Works with existing React patterns and TypeScript -- **Isolated execution** - Each hook runs in its own isolated environment -- **Argument updates** - Update hook arguments dynamically without remounting - -## Quick Start - -```tsx -import { useRemoteHook } from '@scalprum/react-core'; -import { useMemo } from 'react'; - -function MyComponent() { - // ⚠️ Use useMemo when args contain objects/arrays - const args = useMemo(() => [{ initialValue: 0, step: 1 }], []); - - const { hookResult, loading, error } = useRemoteHook({ - scope: 'counter-app', - module: './useCounter', - args - }); - - if (loading) return
Loading...
; - if (error) return
Error: {error.message}
; - - return ( -
-

Count: {hookResult?.count}

- -
- ); -} -``` - -## Documentation - -### Core Components - -- **[RemoteHookProvider](./remote-hook-provider.md)** - Context provider that enables remote hook functionality -- **[useRemoteHook](./use-remote-hook.md)** - Hook for loading and using individual remote hooks -- **[useRemoteHookManager](./use-remote-hook-manager.md)** - Hook for managing multiple remote hooks dynamically -- **[Remote Hook Types](./remote-hook-types.md)** - TypeScript interfaces and type definitions - -### Getting Started - -1. **Setup**: The `RemoteHookProvider` is automatically included when using `ScalprumProvider` -2. **Basic Usage**: Use `useRemoteHook` for single hooks or `useRemoteHookManager` for multiple hooks -3. **Argument Handling**: Remember to memoize arguments containing objects or arrays -4. **Error Handling**: Always handle loading and error states - -## Architecture - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ ScalprumProvider β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ -β”‚ β”‚ RemoteHookProvider β”‚β”‚ -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚β”‚ -β”‚ β”‚ β”‚ Your Components β”‚β”‚β”‚ -β”‚ β”‚ β”‚ - useRemoteHook β”‚β”‚β”‚ -β”‚ β”‚ β”‚ - useRemoteHookManager β”‚β”‚β”‚ -β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Federated Modules β”‚ -β”‚ - ./useCounter β”‚ -β”‚ - ./useApiData β”‚ -β”‚ - ./useTimer β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -## Key Features - -### πŸ”„ Dynamic Loading -Load hooks from remote modules on-demand, enabling modular architecture. - -### 🎯 Isolated Execution -Each remote hook runs in its own isolated environment, preventing conflicts. - -### ⚑ Argument Updates -Update hook arguments dynamically without remounting components. - -### πŸ›‘οΈ Type Safety -Full TypeScript support with generic types for hook results. - -### 🧹 Automatic Cleanup -Proper resource management and memory leak prevention. - -### πŸ“Š Multiple Hook Management -Manage multiple remote hooks dynamically with the hook manager. - -## Common Use Cases - -### πŸ”Œ Plugin Systems -```tsx -// Load different plugin hooks based on user configuration -const { hookResult } = useRemoteHook({ - scope: pluginConfig.scope, - module: pluginConfig.hookModule, - args: [pluginConfig.settings] -}); -``` - -### πŸ“Š Dashboard Widgets -```tsx -// Each widget can use a different remote hook -function Widget({ widgetConfig }) { - const { hookResult, loading } = useRemoteHook({ - scope: widgetConfig.app, - module: widgetConfig.dataHook, - args: [widgetConfig.params] - }); - - return loading ? : ; -} -``` - -### πŸ”„ A/B Testing -```tsx -// Load different hook implementations for testing -const hookModule = isTestVariantA ? './useFeatureA' : './useFeatureB'; - -const { hookResult } = useRemoteHook({ - scope: 'feature-app', - module: hookModule, - args: [userConfig] -}); -``` - -### πŸŽ›οΈ Dynamic Feature Loading -```tsx -// Users can enable/disable features that load different hooks -function FeatureManager() { - const manager = useRemoteHookManager(); - - const enableFeature = (featureName) => { - manager.addHook({ - scope: 'features-app', - module: `./use${featureName}Hook`, - args: [{ enabled: true }] - }); - }; - - return ( -
- - -
- ); -} -``` - -## Performance Tips - -### ⚠️ Critical: Argument Memoization -```tsx -// ❌ Wrong - causes infinite re-renders -const { hookResult } = useRemoteHook({ - scope: 'app', - module: './hook', - args: [{ config: 'value' }] // New object every render! -}); - -// βœ… Correct - memoized arguments -const args = useMemo(() => [{ config: 'value' }], []); -const { hookResult } = useRemoteHook({ - scope: 'app', - module: './hook', - args -}); - -// βœ… Also correct - primitive values don't need memoization -const { hookResult } = useRemoteHook({ - scope: 'app', - module: './hook', - args: [1, 'hello', true] // Primitives are safe -}); -``` - -### 🎯 Efficient Hook Management -- Use `useRemoteHook` for static single hooks -- Use `useRemoteHookManager` for dynamic multi-hook scenarios -- Always clean up hooks when components unmount -- Handle loading and error states appropriately - -## Migration Guide - -If you're upgrading from a previous version or migrating from other solutions: - -1. **Wrap your app** with `ScalprumProvider` (includes `RemoteHookProvider`) -2. **Replace static imports** with `useRemoteHook` calls -3. **Add loading/error handling** for the asynchronous nature of remote hooks -4. **Memoize complex arguments** to prevent infinite re-renders -5. **Update TypeScript types** to use the provided interfaces - -## Examples Repository - -For complete working examples, see the test applications in this repository: -- `examples/test-app/src/routes/RemoteHooks.tsx` - Basic remote hook usage -- `examples/test-app/src/routes/RemoteHookManager.tsx` - Hook manager examples -- `federation-cdn-mock/src/modules/` - Example remote hook implementations - -## Community and Support - -- **Issues**: Report bugs and request features on GitHub -- **Discussions**: Join the community discussions for help and best practices -- **Contributing**: See the contributing guide for development setup - -## See Also - -- [Scalprum Core Documentation](../README.md) - Main Scalprum documentation -- [Module Federation Guide](https://webpack.js.org/concepts/module-federation/) - Understanding federated modules -- [React Hooks Documentation](https://react.dev/reference/react) - React hooks fundamentals \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9d3bbbb..5dbc369 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37249,7 +37249,7 @@ }, "packages/core": { "name": "@scalprum/core", - "version": "0.8.2", + "version": "0.8.3", "license": "Apache-2.0", "dependencies": { "@openshift/dynamic-plugin-sdk": "^5.0.1", @@ -37258,11 +37258,11 @@ }, "packages/react-core": { "name": "@scalprum/react-core", - "version": "0.9.4", + "version": "0.9.5", "license": "Apache-2.0", "dependencies": { "@openshift/dynamic-plugin-sdk": "^5.0.1", - "@scalprum/core": "^0.8.2", + "@scalprum/core": "^0.8.3", "lodash": "^4.17.0" }, "devDependencies": { @@ -37276,12 +37276,12 @@ }, "packages/react-test-utils": { "name": "@scalprum/react-test-utils", - "version": "0.2.5", + "version": "0.2.6", "license": "Apache-2.0", "dependencies": { "@openshift/dynamic-plugin-sdk": "^5.0.1", - "@scalprum/core": "^0.8.2", - "@scalprum/react-core": "^0.9.4", + "@scalprum/core": "^0.8.3", + "@scalprum/react-core": "^0.9.5", "tslib": "^2.6.2", "whatwg-fetch": "^3.6.0" }, diff --git a/packages/build-utils/README.md b/packages/build-utils/README.md index 0e08d71..9345ed5 100644 --- a/packages/build-utils/README.md +++ b/packages/build-utils/README.md @@ -1,11 +1,440 @@ -# build-utils +# @scalprum/build-utils -This library was generated with [Nx](https://nx.dev). +**Build tools and NX executors for Scalprum projects** -## Building +The `@scalprum/build-utils` package provides NX executors and utilities for building, bundling, and managing Scalprum-based micro-frontend projects. It streamlines the build process and automates dependency synchronization across your monorepo. -Run `nx build build-utils` to build the library. +## Installation -## Running unit tests +```bash +npm install @scalprum/build-utils --save-dev +``` -Run `nx test build-utils` to execute the unit tests via [Jest](https://jestjs.io). +## Key Features + +- **Builder Executor**: Custom TypeScript build executor with ESM and CJS support +- **Dependency Sync**: Automatic workspace dependency synchronization +- **NX Integration**: Seamless integration with NX monorepo workflows +- **TypeScript Support**: Full TypeScript compilation with dual module output +- **Asset Management**: Automated asset copying during builds + +## NX Executors + +### builder + +Custom build executor that compiles TypeScript projects with both ESM and CommonJS outputs. + +#### Configuration + +Add to your `project.json`: + +```json +{ + "targets": { + "build": { + "executor": "@scalprum/build-utils:builder", + "options": { + "esmTsConfig": "packages/my-package/tsconfig.esm.json", + "cjsTsConfig": "packages/my-package/tsconfig.cjs.json", + "outputPath": "dist/packages/my-package", + "assets": ["packages/my-package/README.md"] + } + } + } +} +``` + +#### Options + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `esmTsConfig` | `string` | Yes | Path to ESM TypeScript config | +| `cjsTsConfig` | `string` | Yes | Path to CJS TypeScript config | +| `outputPath` | `string` | Yes | Output directory path | +| `assets` | `string[]` | No | Additional files to copy to output | + +#### Usage + +```bash +nx build my-package +``` + +This executor: +1. Validates all config files exist +2. Compiles TypeScript to ESM format (outputs to `outputPath/esm/`) +3. Compiles TypeScript to CJS format (outputs to `outputPath/`) +4. Copies `package.json` and any specified assets to output directory + +#### Example TypeScript Configs + +**tsconfig.esm.json** +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "target": "es2015", + "declaration": true + } +} +``` + +**tsconfig.cjs.json** +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "declaration": true + } +} +``` + +### sync-dependencies + +Automatically synchronizes workspace package dependencies across your monorepo. Updates package versions when local dependencies are bumped and commits changes to your branch. + +#### Configuration + +Add to your `project.json`: + +```json +{ + "targets": { + "sync-deps": { + "executor": "@scalprum/build-utils:sync-dependencies", + "options": { + "baseBranch": "main", + "remote": "origin" + } + } + } +} +``` + +#### Options + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `baseBranch` | `string` | No | `"main"` | Git branch to push changes to | +| `remote` | `string` | No | `"origin"` | Git remote name | + +#### Usage + +```bash +nx sync-deps my-package +``` + +This executor: +1. Analyzes the NX project dependency graph +2. Finds all workspace dependencies for the current project +3. Checks if dependency versions satisfy current ranges +4. Updates `package.json` with newer versions if available +5. Commits changes with message `"chore: [skip ci] sync dependencies"` +6. Pushes changes to the specified remote branch + +#### How It Works + +The executor uses semantic versioning to determine if dependencies need updating: + +```typescript +// Example: If my-package depends on @scalprum/core +// Current: "@scalprum/core": "^0.8.0" +// Available: @scalprum/core@0.8.3 + +// Executor checks: +// 1. Is 0.8.3 a valid semver? βœ“ +// 2. Does 0.8.3 satisfy ^0.8.0? βœ“ +// 3. Update to: "@scalprum/core": "^0.8.3" +``` + +Only updates versions that: +- Are valid semantic versions +- Satisfy the existing version range +- Preserve the range prefix (^, ~, etc.) + +#### CI/CD Integration + +Perfect for automated workflows: + +```yaml +# .github/workflows/sync-deps.yml +name: Sync Dependencies + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 1' # Weekly on Monday + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: npm install + - run: nx run-many --target=sync-deps --all +``` + +The `[skip ci]` commit message prevents infinite CI loops. + +## Package Structure + +This package follows NX executor conventions: + +``` +@scalprum/build-utils/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ executors/ +β”‚ β”‚ β”œβ”€β”€ builder/ +β”‚ β”‚ β”‚ β”œβ”€β”€ executor.ts +β”‚ β”‚ β”‚ └── schema.json +β”‚ β”‚ └── sync-dependencies/ +β”‚ β”‚ β”œβ”€β”€ executor.ts +β”‚ β”‚ └── schema.json +β”‚ └── index.ts +β”œβ”€β”€ executors.json +└── package.json +``` + +## Dependencies + +```json +{ + "dependencies": { + "@nx/devkit": "^17.1.3", + "semver": "^7.5.4", + "zod": "^3.22.4" + } +} +``` + +- **@nx/devkit**: NX workspace utilities and APIs +- **semver**: Semantic version parsing and comparison +- **zod**: Schema validation for executor options + +## TypeScript Support + +The package is written in TypeScript and provides type definitions: + +```typescript +import { BuilderExecutorSchemaType } from '@scalprum/build-utils'; + +const options: BuilderExecutorSchemaType = { + esmTsConfig: './tsconfig.esm.json', + cjsTsConfig: './tsconfig.cjs.json', + outputPath: 'dist/my-package', + assets: ['README.md', 'LICENSE'] +}; +``` + +## Complete Example + +### Project Setup + +**packages/my-library/project.json** +```json +{ + "name": "my-library", + "sourceRoot": "packages/my-library/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@scalprum/build-utils:builder", + "outputs": ["{options.outputPath}"], + "options": { + "esmTsConfig": "packages/my-library/tsconfig.esm.json", + "cjsTsConfig": "packages/my-library/tsconfig.cjs.json", + "outputPath": "dist/packages/my-library", + "assets": [ + "packages/my-library/README.md", + "packages/my-library/LICENSE" + ] + } + }, + "sync-deps": { + "executor": "@scalprum/build-utils:sync-dependencies", + "options": { + "baseBranch": "main", + "remote": "origin" + } + } + } +} +``` + +**packages/my-library/package.json** +```json +{ + "name": "@myorg/my-library", + "version": "1.0.0", + "main": "./index.js", + "module": "./esm/index.js", + "types": "./index.d.ts", + "dependencies": { + "@scalprum/core": "^0.8.3" + } +} +``` + +### Build Commands + +```bash +# Build single package +nx build my-library + +# Build all packages +nx run-many --target=build --all + +# Sync dependencies for single package +nx sync-deps my-library + +# Sync dependencies for all packages +nx run-many --target=sync-deps --all + +# Watch mode (if configured) +nx build my-library --watch +``` + +## Build Output Structure + +After running the builder, your output directory will contain: + +``` +dist/packages/my-library/ +β”œβ”€β”€ esm/ # ESM modules +β”‚ β”œβ”€β”€ index.js +β”‚ └── index.d.ts +β”œβ”€β”€ index.js # CJS modules +β”œβ”€β”€ index.d.ts # Type definitions +β”œβ”€β”€ package.json # Copied from source +β”œβ”€β”€ README.md # Copied asset +└── LICENSE # Copied asset +``` + +## Advanced Usage + +### Custom Build Scripts + +You can wrap the executors in npm scripts: + +**package.json** +```json +{ + "scripts": { + "build": "nx run-many --target=build --all --parallel", + "build:prod": "nx run-many --target=build --all --configuration=production", + "sync-deps": "nx run-many --target=sync-deps --all" + } +} +``` + +### Conditional Asset Copying + +Only copy assets that exist: + +```json +{ + "assets": [ + "packages/my-library/README.md", + "packages/my-library/CHANGELOG.md" + ] +} +``` + +Missing files are silently skipped. + +### Integration with Other Executors + +Combine with other NX executors: + +```json +{ + "targets": { + "build": { + "executor": "@scalprum/build-utils:builder", + "options": { /* ... */ } + }, + "test": { + "executor": "@nx/jest:jest", + "options": { /* ... */ } + }, + "lint": { + "executor": "@nx/linter:eslint", + "options": { /* ... */ } + }, + "sync-deps": { + "executor": "@scalprum/build-utils:sync-dependencies" + } + } +} +``` + +## Troubleshooting + +### Build Fails: TypeScript Config Not Found + +``` +Error: ENOENT: no such file or directory +``` + +**Solution:** Ensure both `esmTsConfig` and `cjsTsConfig` paths are correct and files exist. + +### Dependency Sync No Changes + +The sync executor only updates versions that: +1. Are valid semantic versions +2. Satisfy the current version range +3. Are newer than what's currently specified + +If no updates occur, dependencies are already at the latest satisfying version. + +### Git Push Fails + +``` +Error: failed to push some refs +``` + +**Solution:** Ensure the executor has permissions to push to the remote branch. In CI/CD, configure git credentials: + +```bash +git config --global user.email "bot@example.com" +git config --global user.name "Dependency Bot" +``` + +## Best Practices + +1. **Separate TS Configs**: Maintain separate `tsconfig.esm.json` and `tsconfig.cjs.json` for clarity +2. **Asset Management**: Include README and LICENSE in assets for published packages +3. **Automated Sync**: Run `sync-dependencies` in CI for automatic version updates +4. **Output Path Convention**: Use `dist/packages/{package-name}` for consistency +5. **Skip CI**: The sync executor includes `[skip ci]` to prevent infinite loops + +## Build Tool Compatibility + +This package is designed for: + +- **NX Workspaces** (v17+) +- **TypeScript** (v4+) +- **Module Federation** projects (via proper build configuration) + +## Related Packages + +- [`@scalprum/core`](../core) - Framework-agnostic core library +- [`@scalprum/react-core`](../react-core) - React bindings and components +- [`@scalprum/react-test-utils`](../react-test-utils) - Testing utilities + +## Contributing + +When adding new executors: + +1. Create executor directory in `src/executors/{executor-name}/` +2. Add `executor.ts` with default export function +3. Add `schema.json` with Zod validation +4. Update `executors.json` with executor registration +5. Document options and usage in this README + +## License + +Apache-2.0 diff --git a/packages/core/README.md b/packages/core/README.md index f89ff55..92faf68 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1 +1,200 @@ # @scalprum/core + +**Framework-agnostic core for micro-frontend module federation** + +The `@scalprum/core` package provides the foundational module federation capabilities for Scalprum. It's a framework-agnostic library that handles dynamic module loading, caching, and manifest processing - making it compatible with any JavaScript framework, not just React. + +## Installation + +```bash +npm install @scalprum/core +``` + +## Key Features + +- **Framework Agnostic**: Works with any JavaScript framework or vanilla JS +- **Dynamic Module Loading**: Load remote modules at runtime with caching +- **Manifest Processing**: Support for both plugin manifests and custom formats +- **Shared Scope Management**: Integration with webpack's module federation shared scopes +- **Built-in Caching**: Intelligent module caching with configurable timeout +- **Error Handling**: Robust error handling for network and module loading failures + +## Basic Usage + +```typescript +import { initialize, getModule, AppsConfig } from '@scalprum/core'; + +// Configure your remote modules +const config: AppsConfig = { + myRemoteApp: { + name: 'myRemoteApp', + manifestLocation: 'http://localhost:3001/plugin-manifest.json' + } +}; + +// Initialize Scalprum +const scalprum = initialize({ + appsConfig: config, + api: { /* shared context */ } +}); + +// Load a module dynamically +const MyComponent = await getModule('myRemoteApp', 'MyComponent'); +``` + +## API Reference + +### Core Functions + +#### `initialize(options)` + +Initializes the Scalprum instance with configuration. + +**Parameters:** +- `appsConfig: AppsConfig` - Configuration for remote modules +- `api?: any` - Shared API context available to all modules +- `options?: Partial` - Optional configuration +- `pluginStoreFeatureFlags?: FeatureFlags` - Feature flags for plugin store +- `pluginLoaderOptions?: PluginLoaderOptions` - Options for plugin loader +- `pluginStoreOptions?: PluginStoreOptions` - Options for plugin store + +**Returns:** `Scalprum` instance + +#### `getModule(scope, module, importName?)` + +Loads a module from a remote container. + +**Parameters:** +- `scope: string` - The remote container name +- `module: string` - The module name to load +- `importName?: string` - Specific export name (defaults to 'default') + +**Returns:** `Promise` - The loaded module + +#### `getScalprum()` + +Gets the current Scalprum instance. + +**Returns:** `Scalprum` instance + +**Throws:** Error if Scalprum hasn't been initialized + +### Configuration Types + +#### `AppsConfig` + +```typescript +interface AppsConfig { + [key: string]: AppMetadata; +} + +type AppMetadata = T & { + name: string; + appId?: string; + elementId?: string; + rootLocation?: string; + scriptLocation?: string; + manifestLocation?: string; + pluginManifest?: PluginManifest; +}; +``` + +#### `ScalprumOptions` + +```typescript +interface ScalprumOptions { + cacheTimeout: number; // Cache timeout in seconds (default: 120) + enableScopeWarning: boolean; // Enable duplicate package warnings +} +``` + +## Advanced Usage + +### Preloading Modules + +```typescript +import { preloadModule } from '@scalprum/core'; + +// Preload a module without importing it +await preloadModule('myRemoteApp', 'MyComponent'); + +// With custom manifest processor +await preloadModule('myRemoteApp', 'MyComponent', (manifest) => manifest.assets.js); + +// Later, get the cached module instantly +const MyComponent = await getModule('myRemoteApp', 'MyComponent'); +``` + +### Custom Manifest Processing + +```typescript +const processor = (manifest: any) => { + // Extract entry scripts from custom manifest format + return manifest.assets.js; +}; + +await processManifest( + 'http://localhost:3001/custom-manifest.json', + 'myScope', + 'MyModule', + processor +); +``` + +### Module Caching + +```typescript +import { getCachedModule } from '@scalprum/core'; + +// Check if module is cached +const { cachedModule, prefetchPromise } = getCachedModule('myScope', 'MyModule'); + +if (cachedModule) { + // Module is cached and ready + const component = cachedModule.default; +} +``` + +## Shared Scope Integration + +Scalprum integrates with webpack's module federation shared scopes: + +```typescript +import { initSharedScope, getSharedScope } from '@scalprum/core'; + +// Initialize shared scope +await initSharedScope(); + +// Get shared scope object +const sharedScope = getSharedScope(true); // true enables duplicate warnings +``` + +## Error Handling + +```typescript +try { + const module = await getModule('myScope', 'NonExistentModule'); +} catch (error) { + if (error.message.includes('Module not initialized')) { + // Module wasn't found in the remote container + console.error('Module not available:', error); + } else if (error.message.includes('Manifest location not found')) { + // Scope configuration is missing manifestLocation + console.error('Configuration error:', error); + } +} +``` + +## Build Tool Compatibility + +This package is compatible with: + +- **Webpack 5** with Module Federation +- **Rspack** with Module Federation support +- **Module Federation Runtime** for any bundler + +## Related Packages + +- [`@scalprum/react-core`](../react-core) - React bindings and components +- [`@scalprum/build-utils`](../build-utils) - Build tools and NX executors +- [`@scalprum/react-test-utils`](../react-test-utils) - Testing utilities \ No newline at end of file diff --git a/packages/react-core/README.md b/packages/react-core/README.md index 1053f99..7bdf45a 100644 --- a/packages/react-core/README.md +++ b/packages/react-core/README.md @@ -1,11 +1,597 @@ -# react-core +# @scalprum/react-core -This library was generated with [Nx](https://nx.dev). +**React bindings for Scalprum module federation** -## Building +The `@scalprum/react-core` package provides React-specific components, hooks, and utilities for building micro-frontend applications with Scalprum. It wraps the framework-agnostic `@scalprum/core` with an idiomatic React API. -Run `nx build react-core` to build the library. +## Installation -## Running unit tests +```bash +npm install @scalprum/react-core @scalprum/core react react-dom +``` -Run `nx test react-core` to execute the unit tests via [Jest](https://jestjs.io). +## Key Features + +- **ScalprumProvider**: Context provider for Scalprum configuration +- **Component Loading**: Declarative components for loading remote modules +- **React Hooks**: Modern hooks API for accessing remote modules +- **Remote Hooks**: Load and execute hooks from federated modules +- **Prefetching Support**: Built-in data prefetching capabilities +- **Error Boundaries**: Automatic error handling with self-repair +- **TypeScript Support**: Full type safety for remote modules + +## Quick Start + +```tsx +import { ScalprumProvider, ScalprumComponent } from '@scalprum/react-core'; + +const config = { + myApp: { + name: 'myApp', + manifestLocation: 'http://localhost:3001/plugin-manifest.json' + } +}; + +function App() { + return ( + + Loading...
} + /> + + ); +} +``` + +## Core Components + +### ScalprumProvider + +The root provider that initializes Scalprum and provides context to all child components. + +```tsx +import { ScalprumProvider } from '@scalprum/react-core'; + +function App() { + const config = { + remoteApp: { + name: 'remoteApp', + manifestLocation: 'http://localhost:3001/plugin-manifest.json' + } + }; + + const api = { + user: { id: '123', name: 'John' }, + theme: 'dark' + }; + + return ( + + {/* Your app */} + + ); +} +``` + +**Props:** +- `config: AppsConfig` - Configuration for remote modules +- `api?: T` - Shared API context available to all modules +- `pluginSDKOptions?` - Optional plugin SDK configuration + +### ScalprumComponent + +Declarative component for loading and rendering remote modules. + +```tsx +import { ScalprumComponent } from '@scalprum/react-core'; + +function Dashboard() { + return ( + } + ErrorComponent={} + someProp="value" // Props are passed to remote component + /> + ); +} +``` + +**Props:** +- `scope: string` - Remote container name +- `module: string` - Module name to load +- `importName?: string` - Specific export (default: 'default') +- `fallback?` - Loading fallback UI +- `ErrorComponent?` - Custom error component +- Additional props are forwarded to the remote component + +**Features:** +- Automatic error boundaries +- Self-repair on cache errors +- Suspense integration +- Prefetch support + +## React Hooks + +### useScalprum + +Access the Scalprum context and API. + +```tsx +import { useScalprum } from '@scalprum/react-core'; + +function MyComponent() { + const { config, api, initialized } = useScalprum(); + + if (!initialized) { + return
Initializing...
; + } + + return
User: {api.user.name}
; +} + +// Using optional selector for optimized re-renders +function OptimizedComponent() { + const api = useScalprum(state => state.api); + return
User: {api.user.name}
; +} +``` + +**Parameters:** +- `selector?: (state: ScalprumState) => T` - Optional selector function to extract specific state + +**Returns:** +- `config` - Apps configuration +- `api` - Shared API context +- `initialized` - Whether Scalprum is ready +- `pluginStore` - Plugin store instance + +### useModule + +Hook for loading remote modules programmatically. + +```tsx +import { useModule } from '@scalprum/react-core'; + +function WidgetContainer() { + const Widget = useModule('widgets', 'PieChart'); + + if (!Widget) { + return
Loading widget...
; + } + + return ; +} +``` + +**Parameters:** +- `scope: string` - Remote container name +- `module: string` - Module name +- `defaultState?` - Initial state while loading +- `importName?: string` - Export name (default: 'default') + +**Returns:** The loaded module or `defaultState` + +### useLoadModule + +Advanced hook for loading modules with more control. + +```tsx +import { useLoadModule } from '@scalprum/react-core'; + +function DataDisplay() { + const [DataTable, error] = useLoadModule({ + scope: 'tables', + module: 'DataGrid' + }, undefined); + + if (error) return ; + if (!DataTable) return ; + + return ; +} +``` + +### usePrefetch + +Hook for prefetching data from remote modules. + +```tsx +import { usePrefetch } from '@scalprum/react-core'; + +function DataComponent() { + const { ready, data, error } = usePrefetch(); + + if (!ready) return
Loading data...
; + + return
{JSON.stringify(data)}
; +} +``` + +## Remote Hooks + +Scalprum supports loading and executing React hooks from federated modules, enabling advanced micro-frontend patterns. + +### RemoteHookProvider + +The `RemoteHookProvider` is automatically included in `ScalprumProvider` - no additional setup required. + +```tsx + + {/* Remote hooks work automatically */} + + +``` + +### useRemoteHook + +Load and execute hooks from remote federated modules. + +```tsx +import { useRemoteHook } from '@scalprum/react-core'; +import { useMemo } from 'react'; + +interface CounterResult { + count: number; + increment: () => void; + decrement: () => void; +} + +function MyComponent() { + // IMPORTANT: Use useMemo when args contain objects/arrays + const args = useMemo(() => [{ initialValue: 0, step: 1 }], []); + + const { hookResult, loading, error } = useRemoteHook({ + scope: 'counter-app', + module: './useCounter', + args + }); + + if (loading) return
Loading hook...
; + if (error) return
Error: {error.message}
; + + return ( +
+

Count: {hookResult?.count}

+ + +
+ ); +} +``` + +**Parameters:** +- `scope: string` - Federated module scope +- `module: string` - Module path +- `importName?: string` - Named export (optional) +- `args?: any[]` - Arguments to pass (must be memoized if containing objects/arrays) + +**Returns:** +- `id: string` - Unique hook instance ID +- `loading: boolean` - Loading state +- `error: Error | null` - Error if any +- `hookResult?: T` - Hook execution result + +**Critical:** When `args` contains objects or arrays, always use `useMemo` to prevent infinite re-renders. + +### useRemoteHookManager + +Manage multiple remote hooks dynamically. + +```tsx +import { useRemoteHookManager } from '@scalprum/react-core'; +import { useMemo } from 'react'; + +function DynamicHooksComponent() { + const manager = useRemoteHookManager(); + + const addCounter = () => { + const handle = manager.addHook({ + scope: 'counter-app', + module: './useCounter', + args: [{ initialValue: 0, step: 1 }] + }); + + // Update args later + handle.updateArgs([{ initialValue: 10, step: 2 }]); + + // Remove when done + // handle.remove(); + }; + + const results = manager.getHookResults(); + + return ( +
+ +
Active hooks: {results.length}
+ {results.map(({ id, hookResult, loading, error }) => ( +
+ {loading && Loading...} + {error && Error: {error.message}} + {hookResult && Count: {hookResult.count}} +
+ ))} +
+ ); +} +``` + +**Methods:** +- `addHook(config)` - Add a new remote hook, returns handle +- `cleanup()` - Remove all hooks (called automatically on unmount) +- `getHookResults()` - Get results from all tracked hooks + +**Handle Methods:** +- `remove()` - Remove this specific hook +- `updateArgs(args)` - Update hook arguments + +For detailed remote hooks documentation, see: +- [useRemoteHook Guide](./docs/use-remote-hook.md) +- [useRemoteHookManager Guide](./docs/use-remote-hook-manager.md) +- [RemoteHookProvider Reference](./docs/remote-hook-provider.md) +- [Remote Hook Types](./docs/remote-hook-types.md) + +## Complete Example + +```tsx +import { + ScalprumProvider, + ScalprumComponent, + useScalprum, + useModule, + useRemoteHook +} from '@scalprum/react-core'; +import { useMemo } from 'react'; + +// Configuration +const config = { + dashboard: { + name: 'dashboard', + manifestLocation: 'http://localhost:3001/plugin-manifest.json' + }, + widgets: { + name: 'widgets', + manifestLocation: 'http://localhost:3002/plugin-manifest.json' + } +}; + +const api = { + user: { id: '123', name: 'John Doe' }, + permissions: ['read', 'write'] +}; + +// Using declarative component +function DashboardView() { + return ( + Loading dashboard...} + /> + ); +} + +// Using hooks +function WidgetPanel() { + const { api } = useScalprum(); + const ChartWidget = useModule('widgets', 'ChartComponent'); + + if (!ChartWidget) { + return
Loading widget...
; + } + + return ; +} + +// Using remote hooks +function RemoteHookExample() { + const args = useMemo(() => [{ userId: '123' }], []); + + const { hookResult, loading, error } = useRemoteHook({ + scope: 'dashboard', + module: './useUserData', + args + }); + + if (loading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return
Welcome, {hookResult?.name}!
; +} + +// Main App +function App() { + return ( + +
+ + + +
+
+ ); +} +``` + +## Error Handling + +ScalprumComponent includes automatic error boundaries with self-repair: + +```tsx +import { ScalprumComponent } from '@scalprum/react-core'; + +function CustomError({ error, errorInfo }) { + return ( +
+

Failed to load component

+

{error?.message}

+
{errorInfo?.componentStack}
+
+ ); +} + +function App() { + return ( + } + /> + ); +} +``` + +**Self-Repair Feature:** If a component fails to load, ScalprumComponent automatically retries once with cache disabled. + +## TypeScript Support + +Full type safety for remote modules and hooks: + +```tsx +import { ScalprumProvider, useModule, useRemoteHook } from '@scalprum/react-core'; + +interface WidgetProps { + title: string; + data: number[]; +} + +interface UserHookResult { + user: { id: string; name: string }; + loading: boolean; +} + +function TypedExample() { + // Typed remote component + const Widget = useModule>('widgets', 'Chart'); + + // Typed remote hook + const { hookResult } = useRemoteHook({ + scope: 'auth', + module: './useCurrentUser' + }); + + if (!Widget || !hookResult) return null; + + return ; +} +``` + +## Build Tool Compatibility + +This package works with: + +- **Webpack 5** with Module Federation plugin +- **Rspack** with Module Federation support +- **Module Federation Runtime** for any bundler + +## Prefetching + +Components can export a `prefetch` function to load data before rendering: + +```tsx +// In remote module +export const prefetch = (api) => { + return fetch(`/api/data?user=${api.user.id}`).then(r => r.json()); +}; + +export default function MyComponent({ data }) { + // Component receives prefetched data + return
{data.value}
; +} +``` + +```tsx +// In host app +import { usePrefetch } from '@scalprum/react-core'; + +function DataComponent() { + const { ready, data, error } = usePrefetch(); + + if (!ready) return
Loading...
; + + return
{data.value}
; +} +``` + +## Advanced Configuration + +### Plugin SDK Options + +```tsx + ({ + ...manifest, + loadScripts: manifest.loadScripts.map(s => `${manifest.baseURL}${s}`) + }) + } + }} +> + {/* Your app */} + +``` + +### Custom Manifest Processing + +```tsx + manifest.assets.js} +/> +``` + +## API Reference + +### Exports + +```tsx +// Components +export { ScalprumProvider } from './scalprum-provider'; +export { ScalprumComponent } from './scalprum-component'; + +// Hooks +export { useScalprum } from './use-scalprum'; +export { useModule } from './use-module'; +export { useLoadModule } from './use-load-module'; +export { usePrefetch } from './use-prefetch'; + +// Remote Hooks +export { useRemoteHook } from './use-remote-hook'; +export { useRemoteHookManager } from './use-remote-hook-manager'; +export { RemoteHookProvider } from './remote-hook-provider'; + +// Context +export { ScalprumContext } from './scalprum-context'; +export { PrefetchContext } from './prefetch-context'; + +// Types +export * from './remote-hooks-types'; +``` + +## Related Packages + +- [`@scalprum/core`](../core) - Framework-agnostic core library +- [`@scalprum/build-utils`](../build-utils) - Build tools and NX executors +- [`@scalprum/react-test-utils`](../react-test-utils) - Testing utilities for React components + +## Documentation + +- [Getting Started Guide](./docs/getting-started.md) +- [useRemoteHook Documentation](./docs/use-remote-hook.md) +- [useRemoteHookManager Documentation](./docs/use-remote-hook-manager.md) +- [RemoteHookProvider Reference](./docs/remote-hook-provider.md) +- [Remote Hook Types](./docs/remote-hook-types.md) + +## License + +Apache-2.0 diff --git a/packages/react-core/docs/getting-started.md b/packages/react-core/docs/getting-started.md new file mode 100644 index 0000000..e6b12e8 --- /dev/null +++ b/packages/react-core/docs/getting-started.md @@ -0,0 +1,507 @@ +# Getting Started with Scalprum + +This comprehensive tutorial will walk you through setting up a complete Scalprum micro-frontend environment from scratch. + +## Prerequisites + +- Ensure that you have [Node.js](https://nodejs.org/en/download/) installed. +- Environment using webpack 5 to build the final output +- Using React frontend library + +## Demo Environment Setup + +The following steps outline an example webpack development setup for demo purposes. +You can adjust the steps to suit your own development requirements. + +1. Create a working directory for the project: +```sh +mkdir scalprum-demo && cd scalprum-demo +``` +2. Use the following command to initialise a node. +```sh +npm init +``` +3. Optional: Depending on your needs, you might want to create a Git repository: +```sh +git init +``` +4. Create a webpack project to set up the development dependencies: +```sh +npm i --save-dev webpack webpack-cli webpack-dev-server swc-loader +``` +5. Install swc-loader so that the webpack can understand react: +```sh +npm i --save-dev swc-loader @swc/core +``` +6. Generate a default webpack configuration, and enter 'y' to any options you want to include: +```sh +npx webpack init +``` +7. Edit the `tsconfig.json` file to match the following configuration: +```js + +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "module": "ESNext", + "target": "ESNext", + "allowJs": true, + "moduleResolution": "node", + "jsx": "react", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + }, + "include": ["src/**/*tsx", "src/**/*ts"] +} + +``` +8. Install Scalprum and its dependencies: +```sh +npm i react react-dom @scalprum/core @scalprum/react-core +``` +9. Add a type definition for react: +```sh +npm i --save-dev @types/react-dom @types/react +``` +10. Edit the `index.html` file to add the root element also to the html: +```htmlembedded= + + + + + Webpack App + + +
+ + +``` + +11. Edit the `webpack.config.js` file to match the following configuration: +```js +// Generated using webpack-cli https://github.com/webpack/webpack-cli + +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const { ModuleFederationPlugin } = require('webpack').container; + +const isProduction = process.env.NODE_ENV == "production"; + +const stylesHandler = MiniCssExtractPlugin.loader; + +const config = { + entry: "./src/index.ts", + output: { + path: path.resolve(__dirname, "dist"), + }, + devServer: { + open: true, + host: "localhost", + }, + plugins: [ + new HtmlWebpackPlugin({ + template: "index.html", + }), + new MiniCssExtractPlugin(), + new ModuleFederationPlugin({ + name: 'shell', + filename: isProduction ? 'shell-entry.[contenthash].js' : 'shell-entry.js', + shared: [ + { + react: { + requiredVersion: '*', + singleton: true, + }, + 'react-dom': { + requiredVersion: '*', + singleton: true, + }, + '@scalprum/react-core': { singleton: true, requiredVersion: '*' }, + '@openshift/dynamic-plugin-sdk': { singleton: true, requiredVersion: '*' }, + }, + ], + }), + ], + module: { + rules: [ + { + test: /\.(ts|tsx)$/i, + use: { + loader: 'swc-loader', + options: { + jsc: { + parser: { + syntax: 'typescript', + tsx: true + } + } + } + }, + exclude: ["/node_modules/"], + }, + { + test: /\.css$/i, + use: [stylesHandler, "css-loader"], + }, + { + test: /\.s[ac]ss$/i, + use: [stylesHandler, "css-loader", "sass-loader"], + }, + { + test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, + type: "asset", + }, + + // Add your rules for custom modules here + // Learn more about loaders from https://webpack.js.org/loaders/ + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".jsx", ".js", "..."], + }, +}; + +module.exports = () => { + if (isProduction) { + config.mode = "production"; + } else { + config.mode = "development"; + } + return config; +}; +``` +12. Change the `index.ts` file to match the following example: + +```ts +import('./bootstrap.tsx') +``` +Create a `bootstrap.tsx` file with this code: + +```js + +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +const App = () => { + return ( +
I am an app
+ ) +} + +const domNode = document.getElementById('root'); +if(domNode) { + const root = createRoot(domNode); + root.render() +} + +``` + +If everything is set up, you can run `npm run serve` and view the example text that you entered in the `
` of `index.tsx`. + +## Scalprum Setup + +To use Scalprum, we must create a **host application**. + +A *host application* is the main provider that manages module loading, routing, data sharing, and related tasks. It functions as the manager of the micro-frontend. +The host application always loads first. +It can be any React application as long as it has some mandatory webpack configuration. + +1. To create a host application - a top-level application to manage the micro frontend, in the `scalprum-demo/src` directory, create a `ScalprumRoot.tsx` file, and edit to match the following configuration: +```jsx + +import React from 'react'; +import { AppsConfig } from '@scalprum/core' +import { ScalprumProvider, ScalprumComponent } from '@scalprum/react-core' + +const config: AppsConfig = { + remoteModule: { + name: 'remoteModule', + manifestLocation: 'http://localhost:8003/plugin-manifest.json' + } +} + +const ScalprumRoot = () => { + return ( +
+ + + +
+ ) +} + +export default ScalprumRoot; +``` + +Let's take a look at what we set up in this example. + + + +| Component | Definition | +| -------- | -------- | +| **`AppsConfig module`** | The **`AppsConfig`** is a set of dynamic modules from '@scalprum/core'. This is framework-agnostic and will work as long as it is in Javascript. | +|**`@scalprum/core`** package| The **`@scalprum/core`** package is responsible for managing federated modules. It provides an abstraction on low-level API to make it more developer friendly alongside additional features. Behind the scenes, it uses our exposed webpack modules, **`@openshift/dynamic-plugin-sdk`**, to manage plugins.| +|**`ScalprumProvider`** module|The **`ScalprumProvider`** is the main root component. If you want to load modules, you must ensure they are enclosed in the `` components. `ScalprumProvider` has a mandatory `config` field. This informs `ScalprumProvider` of which remote modules are available to render. For the purpose of the demo, `const config: AppsConfig = {}`, we set up `remoteModule` to use in the next steps. The config requires a `name`, and a `manifestLocation`.| +|**`@scalprum/react-core`**|The **`@scalprum/react-core`** packages provide react core bindings for `@scalprum/core`.| +|**`ScalprumComponent`**|The **`ScalprumComponent`** is provided by the **`@scalprum/react-core`** packages. It requires two parameters: **`scope`**: the `name` that you set in the `ScalprumProvider`'s `config{}`. **`module`**: the actual module that you want to render. For the purposes of this demo, create `RemoteModuleComponent`. +| + + + +2. Edit the `index.tsx` file and make the following changes: + +```jsx +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import ScalprumRoot from './ScalprumRoot'; + +const App = () => { + return ( + + ) +} + +const domNode = document.getElementById('root'); +if(domNode) { + const root = createRoot(domNode); + root.render() +} +``` + +3. In the `webpack.config.js` file, add the `moduleFederation` plugin to set up the environment for plugin applications to use the context of the host applications: + +```jsx +// declare shared dependencies +const moduleFederationPlugin = new ModuleFederationPlugin({ + name: 'host', + filename: 'host.[contenthash].js', + shared: [ + { + '@openshift/dynamic-plugin-sdk': { + singleton: true, + requiredVersion: '*', + }, + }, + { + '@scalprum/react-core': { + singleton: true, + requiredVersion: '*', + }, + }, + { react: { singleton: true, requiredVersion: '*' } }, + { 'react-dom': { singleton: true, requiredVersion: '*'} }, + // any other packages you wish to share + ], +}); +``` + +Note that for demo purposes, the `requiredVersion` for dependencies in this example is set to `*`, but you can update this to a version that suits your requirements. + +4. In the `webpack.config.js` file, add an entry also for the `moduleFederationPlugin` to the declared plugins: + +```jsx + plugins: [ + new HtmlWebpackPlugin({ + template: "index.html", + }), + new MiniCssExtractPlugin(), + // scalprum required plugin + moduleFederationPlugin, + ], +``` +5. To create the remote module, in the `scalprum-demo` directory, create a **remoteModule** directory. +6. Install the following dependencies: +```shell +npm i --save-dev @openshift/dynamic-plugin-sdk-webpack +``` +7. In the **scalprum-demo/remoteModule** directory, create a `webpack.config.js` file. +8. You can copy the contents of `webpack.config.js` file from the previous steps. +9. In the `webpack.config.js` file, remove the `HtmlWebpackPlugin` because it breaks dynamic modules. +This is a known issue that is being actively worked on. +10. Remove the `moduleFederationPlugin` and replace with the `DynamicRemotePlugin`: +```jsx +// declare shared dependencies + +const { DynamicRemotePlugin } = require('@openshift/dynamic-plugin-sdk-webpack'); + +const sharedModules = { + '@openshift/dynamic-plugin-sdk': { singleton: true }, + '@scalprum/react-core': { singleton: true }, + react: { singleton: true }, + 'react-dom': { singleton: true }, +}; + +const dynamicPlugin = new DynamicRemotePlugin({ + extensions: [], + sharedModules, + entryScriptfilename: 'remoteModule.[contenthash].js', + pluginMetadata: { + name: 'remoteModule', + version: '1.0.0', + exposedModules: { + RemoteModuleComponent: './remoteModule/src/RemoteModuleComponent.tsx', + }, + extensions: [], + }, +}); + +``` +11. In the `webpack.config.js` file, also replace the `moduleFederationPlugin` with the `dynamicPlugin` plugins: + +```jsx + plugins: [ + + new MiniCssExtractPlugin(), + // scalprum required plugin + dynamicPlugin + , + ], +``` +12. In the `webpack.config.js` file, also add a `publicPath`: +```jsx +const config = { + entry: "./src/index.tsx", + output: { + path: path.resolve(_dirname, "dist"), + publicPath:'http://localhost:8003' + }, +devServer: { + open: true, + host: "localhost", +}, + +``` + +13. In your **scalprum-demo/remoteModule** directory, create a **src** directory. +14. In your **scalprum-demo/remoteModule/src** directory, create an `index.tsx` file. +15. In your **scalprum-demo/remoteModule/src** directory, create a `RemoteModuleComponent.tsx` file and add the following: +```jsx +import React from 'react'; + +const RemoteModuleComponent = () => { + return ( +
+ I am a remote module component; +
+ ) +} + +export default RemoteModuleComponent; + +``` + +16. In the `scalprum-demo/package.json` file, add the following entry and point to `remoteModule/webpack.config.js` as your configuration file: + +```json + +"build:plugin":"webpack --mode=production --node-env=production -c remoteModule/webpack.config.js" + +``` + + +To test if everything is set up correctly, enter the following command: + +```shell +npm run build:plugin +``` + +Normally, your modules would be served via your own content delivery network, but for demo purposes, you can serve it locally from the **dist** directory that is created when you run `npm run build:plugin`. + +```shell +cd scalprum-demo/remoteModule/dist/ && npx http serve . -p 8003 +``` + +If this action completes without error, you can view your plugin manifest at `http://localhost:8003/`. +You can also run the host application via `npm run serve` and check if the dynamic plugin was loaded correctly in the UI. + +## Next Steps + +Congratulations! You've successfully set up a basic Scalprum micro-frontend environment. Here are some next steps to explore: + +### Learn About Remote Hooks + +Scalprum's Remote Hooks feature allows you to share React hooks across micro-frontends, enabling advanced patterns like: +- Dynamic plugin systems +- Shared state management +- Cross-application data fetching + +**Get started with Remote Hooks:** +- [Remote Hooks Overview](../README.md#remote-hooks) - Introduction and use cases +- [useRemoteHook Guide](./use-remote-hook.md) - Load individual hooks from remote modules +- [useRemoteHookManager Guide](./use-remote-hook-manager.md) - Manage multiple hooks dynamically + +### Explore Advanced Features + +- **Error Handling**: Learn about automatic error boundaries and self-repair in ScalprumComponent +- **Type Safety**: Add TypeScript definitions for your remote modules +- **Prefetching**: Implement data prefetching for faster load times +- **Testing**: Use `@scalprum/react-test-utils` for testing federated components + +### Working Examples + +Check out the complete working examples in this repository: +- `examples/test-app/src/routes/RemoteHooks.tsx` - Basic remote hook usage +- `examples/test-app/src/routes/RemoteHookManager.tsx` - Hook manager examples +- `federation-cdn-mock/src/modules/` - Example remote hook implementations + +### Common Patterns + +**Loading Multiple Components:** +```jsx + + + + + +``` + +**Sharing API Context:** +```jsx +const api = { + user: { id: '123', name: 'John' }, + theme: 'dark', + permissions: ['read', 'write'] +}; + + + {/* All remote modules can access api via useScalprum() */} + +``` + +**Using Hooks Programmatically:** +```jsx +import { useModule } from '@scalprum/react-core'; + +function DynamicComponent() { + const RemoteWidget = useModule('myModule', 'Widget'); + + if (!RemoteWidget) return
Loading...
; + + return ; +} +``` + +### Troubleshooting + +**Module not found errors:** +- Verify the `manifestLocation` URL is accessible +- Check that the remote module is built and served +- Ensure Module Federation configuration matches between host and remote + +**Singleton conflicts:** +- Make sure React, ReactDOM, and @scalprum/react-core are marked as singletons in both host and remote +- Use the same major version of shared dependencies + +**CORS issues:** +- If serving from different origins, ensure CORS headers are configured +- Consider using a reverse proxy in development + +### Additional Resources + +- [Package Documentation](../README.md) - Complete API reference +- [Module Federation Docs](https://module-federation.io/) - Learn more about the underlying technology +- [GitHub Discussions](https://github.com/scalprum/scaffolding/discussions) - Community support and best practices \ No newline at end of file diff --git a/docs/remote-hook-provider.md b/packages/react-core/docs/remote-hook-provider.md similarity index 88% rename from docs/remote-hook-provider.md rename to packages/react-core/docs/remote-hook-provider.md index 0278ed7..b6a296d 100644 --- a/docs/remote-hook-provider.md +++ b/packages/react-core/docs/remote-hook-provider.md @@ -2,6 +2,28 @@ The `RemoteHookProvider` is a React context provider that enables remote hook functionality across your application. It must be wrapped around any components that use `useRemoteHook` or `useRemoteHookManager`. +## Quick Reference + +```tsx +import { ScalprumProvider } from '@scalprum/react-core'; + +// RemoteHookProvider is automatically included in ScalprumProvider +function App() { + return ( + + {/* Remote hooks work automatically here */} + + + ); +} +``` + +**Key points:** +- Automatically included in `ScalprumProvider` - no setup needed +- Can be used standalone for advanced use cases +- Provides isolated execution environments for each remote hook +- Manages hook state, lifecycle, and argument updates + ## Overview The RemoteHookProvider manages the execution of remote hooks by: diff --git a/docs/remote-hook-types.md b/packages/react-core/docs/remote-hook-types.md similarity index 92% rename from docs/remote-hook-types.md rename to packages/react-core/docs/remote-hook-types.md index 7a77402..f8eee98 100644 --- a/docs/remote-hook-types.md +++ b/packages/react-core/docs/remote-hook-types.md @@ -1,6 +1,19 @@ # Remote Hook Types -This document provides TypeScript interfaces and types for the remote hooks functionality in Scalprum. +This document provides comprehensive TypeScript interfaces and types for the remote hooks functionality in Scalprum. + +> **Related Documentation:** +> - [useRemoteHook](./use-remote-hook.md) - Using remote hooks +> - [useRemoteHookManager](./use-remote-hook-manager.md) - Managing multiple hooks +> - [RemoteHookProvider](./remote-hook-provider.md) - Context provider + +## Quick Navigation + +- [Hook Configuration](#hook-configuration) - `HookConfig` +- [Hook Results](#hook-results) - `UseRemoteHookResult` +- [Hook Handles](#hook-handles) - `HookHandle`, `RemoteHookHandle` +- [Hook Manager](#hook-manager) - `RemoteHookManager` +- [Common Patterns](#common-type-patterns) - Examples and usage ## Import diff --git a/docs/use-remote-hook-manager.md b/packages/react-core/docs/use-remote-hook-manager.md similarity index 93% rename from docs/use-remote-hook-manager.md rename to packages/react-core/docs/use-remote-hook-manager.md index f5cb92a..6ff9122 100644 --- a/docs/use-remote-hook-manager.md +++ b/packages/react-core/docs/use-remote-hook-manager.md @@ -2,6 +2,39 @@ The `useRemoteHookManager` hook provides a way to dynamically manage multiple remote hooks. Unlike `useRemoteHook` which manages a single hook, the manager allows you to add, remove, and control multiple remote hooks at runtime. +## Quick Reference + +```tsx +import { useRemoteHookManager } from '@scalprum/react-core'; +import { useEffect } from 'react'; + +// For managing multiple remote hooks dynamically +const manager = useRemoteHookManager(); + +// Add hooks dynamically +const handle = manager.addHook({ + scope: 'remote-app', + module: './useMyHook', + args: [{ config: 'value' }] +}); + +// Update hook arguments +handle.updateArgs([{ config: 'newValue' }]); + +// Remove specific hook +handle.remove(); + +// Get all hook results +const results = manager.getHookResults(); + +// Clean up all hooks (do this on unmount) +useEffect(() => () => manager.cleanup(), [manager]); +``` + +**When to use:** Managing multiple remote hooks dynamically, adding/removing hooks at runtime, or building plugin systems. + +**When NOT to use:** For a single static hook - use [useRemoteHook](./use-remote-hook.md) instead for automatic re-renders. + ## Import ```tsx diff --git a/docs/use-remote-hook.md b/packages/react-core/docs/use-remote-hook.md similarity index 94% rename from docs/use-remote-hook.md rename to packages/react-core/docs/use-remote-hook.md index 55097b4..81febc5 100644 --- a/docs/use-remote-hook.md +++ b/packages/react-core/docs/use-remote-hook.md @@ -2,6 +2,26 @@ The `useRemoteHook` is a React hook that allows you to load and use hooks from remote federated modules. It provides a seamless way to consume hooks that are dynamically loaded from other applications or microfrontends. +## Quick Reference + +```tsx +import { useRemoteHook } from '@scalprum/react-core'; +import { useMemo } from 'react'; + +// For single, static remote hooks +const args = useMemo(() => [{ config: 'value' }], []); +const { hookResult, loading, error } = useRemoteHook({ + scope: 'remote-app', + module: './useMyHook', + importName: 'useNamedHook', // optional, for named exports + args +}); +``` + +**When to use:** Loading a single remote hook with a known scope and module at component mount time. + +**When NOT to use:** For managing multiple hooks dynamically - use [useRemoteHookManager](./use-remote-hook-manager.md) instead. + ## Import ```tsx diff --git a/packages/react-core/project.json b/packages/react-core/project.json index 86ff719..3cbf1d4 100644 --- a/packages/react-core/project.json +++ b/packages/react-core/project.json @@ -10,7 +10,10 @@ "outputPath": "dist/packages/react-core", "esmTsConfig": "packages/react-core/tsconfig.esm.json", "cjsTsConfig": "packages/react-core/tsconfig.cjs.json", - "assets": ["packages/react-core/*.md"] + "assets": [ + "packages/react-core/*.md", + "packages/react-core/docs" + ] } }, "lint": { diff --git a/packages/react-test-utils/README.md b/packages/react-test-utils/README.md index b5c4322..dbf4a77 100644 --- a/packages/react-test-utils/README.md +++ b/packages/react-test-utils/README.md @@ -1,11 +1,655 @@ -# react-test-utils +# @scalprum/react-test-utils -This library was generated with [Nx](https://nx.dev). +**Testing utilities for Scalprum React applications** -## Building +The `@scalprum/react-test-utils` package provides comprehensive testing utilities for Scalprum-based React applications. It simplifies testing micro-frontend components by mocking module federation, providing test providers, and setting up the necessary environment for testing federated modules. -Run `nx build react-test-utils` to build the library. +## Installation -## Running unit tests +```bash +npm install @scalprum/react-test-utils --save-dev +``` -Run `nx test react-test-utils` to execute the unit tests via [Jest](https://jestjs.io). +## Key Features + +- **Mock Scalprum Environment**: Complete Scalprum testing environment setup +- **Plugin Data Mocking**: Mock federated modules and plugin manifests +- **Test Provider Component**: Ready-to-use ScalprumProvider for tests +- **Webpack Share Scope Mocking**: Automatic webpack module federation mocking +- **Fetch Polyfill**: Built-in fetch polyfill for test environments +- **Module Mocking**: Easy mocking of remote federated modules + +## Quick Start + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; + +// Initialize Scalprum mocks once per test file +mockScalprum(); + +describe('MyComponent', () => { + it('renders with mocked plugin', () => { + const { TestScalprumProvider } = mockPluginData(); + + render( + + + + ); + + expect(screen.getByTestId('default-module-test-id')).toBeInTheDocument(); + }); +}); +``` + +## Core Utilities + +### mockScalprum() + +Initializes the complete Scalprum testing environment. Call this once at the beginning of your test file. + +```tsx +import { mockScalprum } from '@scalprum/react-test-utils'; + +// Set up Scalprum mocks before tests +mockScalprum(); + +describe('Scalprum Tests', () => { + // Your tests +}); +``` + +**What it does:** +1. Mocks webpack share scope (`__webpack_share_scopes__`) +2. Adds fetch polyfill if not available +3. Sets up necessary global objects for module federation + +### mockWebpackShareScope() + +Mocks the webpack module federation shared scope. Usually called via `mockScalprum()`. + +```tsx +import { mockWebpackShareScope } from '@scalprum/react-test-utils'; + +beforeAll(() => { + mockWebpackShareScope(); +}); +``` + +**Creates:** +```typescript +globalThis.__webpack_share_scopes__ = { + default: {} +}; +``` + +### mockFetch() + +Adds fetch polyfill to the test environment. Usually called via `mockScalprum()`. + +```tsx +import { mockFetch } from '@scalprum/react-test-utils'; + +beforeAll(() => { + mockFetch(); +}); +``` + +### mockPluginData() + +Creates a complete mock setup for testing federated modules with custom configuration. + +```tsx +import { mockPluginData, DEFAULT_MODULE_TEST_ID } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; + +describe('Plugin Module Tests', () => { + it('renders mocked module', () => { + const { TestScalprumProvider, response } = mockPluginData({ + pluginManifest: { + name: 'my-plugin', + version: '1.0.0', + baseURL: 'http://localhost:3001', + loadScripts: ['plugin.js'], + extensions: [], + registrationMethod: 'custom' + }, + module: 'MyExposedComponent', + moduleMock: { + default: () =>
Hello from plugin
+ } + }); + + render( + + + + ); + + expect(screen.getByTestId('my-component')).toBeInTheDocument(); + }); +}); +``` + +#### Parameters + +```typescript +interface MockPluginDataOptions { + headers?: Headers; + url?: string; + type?: ResponseType; + ok?: boolean; + status?: number; + statusText?: string; + pluginManifest?: PluginManifest; + module?: string; + moduleMock?: ModuleMock; + config?: AppsConfig; +} + +type ModuleMock = { + [importName: string]: React.ComponentType; +}; +``` + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `headers` | `Headers` | `new Headers()` | Response headers | +| `url` | `string` | `'http://localhost:3000/test-plugin/plugin-manifest.json'` | Manifest URL | +| `type` | `ResponseType` | `'default'` | Response type | +| `ok` | `boolean` | `true` | Response ok status | +| `status` | `number` | `200` | HTTP status code | +| `statusText` | `string` | `'OK'` | HTTP status text | +| `pluginManifest` | `PluginManifest` | Default manifest | Plugin manifest configuration | +| `module` | `string` | `'ExposedModule'` | Module name to expose | +| `moduleMock` | `ModuleMock` | Default component | Mock module exports | +| `config` | `AppsConfig` | Auto-generated | Apps configuration | + +**Second Parameter:** + +```typescript +api?: ScalprumProviderConfigurableProps['api'] +``` + +Optional API context to pass to ScalprumProvider. + +#### Returns + +```typescript +{ + response: Response; // Mocked fetch response + TestScalprumProvider: React.ComponentType; +} +``` + +## Complete Examples + +### Basic Component Test + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; +import { ScalprumComponent } from '@scalprum/react-core'; + +mockScalprum(); + +describe('ScalprumComponent', () => { + it('loads and renders remote component', async () => { + const { TestScalprumProvider } = mockPluginData({ + pluginManifest: { + name: 'dashboard', + version: '1.0.0', + baseURL: 'http://localhost:3001', + loadScripts: ['dashboard.js'], + extensions: [], + registrationMethod: 'custom' + }, + module: 'Dashboard', + moduleMock: { + default: () =>
Dashboard Content
+ } + }); + + render( + + Loading...
} + /> + + ); + + expect(await screen.findByTestId('dashboard')).toBeInTheDocument(); + }); +}); +``` + +### Custom Module Mock with Props + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; + +mockScalprum(); + +describe('Widget Component', () => { + it('passes props to remote component', async () => { + const WidgetMock = ({ title, data }) => ( +
+

{title}

+

Data points: {data.length}

+
+ ); + + const { TestScalprumProvider } = mockPluginData({ + module: 'Widget', + moduleMock: { + default: WidgetMock + } + }); + + render( + + + + ); + + expect(screen.getByText('Sales Chart')).toBeInTheDocument(); + expect(screen.getByText('Data points: 5')).toBeInTheDocument(); + }); +}); +``` + +### Testing with Custom API Context + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; +import { useScalprum } from '@scalprum/react-core'; + +mockScalprum(); + +function ComponentUsingAPI() { + const { api } = useScalprum(); + return
User: {api.user.name}
; +} + +describe('Component with API', () => { + it('provides custom API context', () => { + const api = { + user: { id: '123', name: 'Test User' }, + theme: 'dark' + }; + + const { TestScalprumProvider } = mockPluginData({}, api); + + render( + + + + ); + + expect(screen.getByText('User: Test User')).toBeInTheDocument(); + }); +}); +``` + +### Testing Multiple Modules + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; + +mockScalprum(); + +describe('Multiple Modules', () => { + it('handles multiple exposed modules', async () => { + const { TestScalprumProvider } = mockPluginData({ + module: 'MainComponent', + moduleMock: { + default: () =>
Main
, + SecondaryComponent: () =>
Secondary
+ } + }); + + function TestApp() { + return ( + <> + + + + ); + } + + render( + + + + ); + + expect(await screen.findByTestId('main')).toBeInTheDocument(); + expect(await screen.findByTestId('secondary')).toBeInTheDocument(); + }); +}); +``` + +### Testing Error States + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen } from '@testing-library/react'; + +mockScalprum(); + +describe('Error Handling', () => { + it('handles failed plugin loading', () => { + const { TestScalprumProvider } = mockPluginData({ + ok: false, + status: 404, + statusText: 'Not Found' + }); + + function ErrorComponent({ error }) { + return
Error: {error?.message}
; + } + + render( + + } + /> + + ); + + // Component should handle the error gracefully + }); +}); +``` + +### Testing with useModule Hook + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import { useModule } from '@scalprum/react-core'; + +mockScalprum(); + +function TestComponent() { + const Widget = useModule('test-plugin', 'Widget'); + + if (!Widget) { + return
Loading...
; + } + + return ; +} + +describe('useModule Hook', () => { + it('loads module with useModule', async () => { + const WidgetMock = ({ title }) =>
{title}
; + + const { TestScalprumProvider } = mockPluginData({ + module: 'Widget', + moduleMock: { + default: WidgetMock + } + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByTestId('widget')).toBeInTheDocument(); + }); + }); +}); +``` + +### Testing with Remote Hooks + +```tsx +import { mockScalprum, mockPluginData } from '@scalprum/react-test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import { useRemoteHook } from '@scalprum/react-core'; +import { useMemo, useState } from 'react'; + +mockScalprum(); + +function ComponentWithRemoteHook() { + const args = useMemo(() => [{ initialValue: 0 }], []); + + const { hookResult, loading, error } = useRemoteHook({ + scope: 'test-plugin', + module: 'useCounter', + args + }); + + if (loading) return
Loading hook...
; + if (error) return
Error: {error.message}
; + + return
Count: {hookResult?.count}
; +} + +describe('Remote Hooks', () => { + it('loads and executes remote hook', async () => { + const useCounterMock = ({ initialValue }) => { + const [count, setCount] = useState(initialValue); + return { count, increment: () => setCount(c => c + 1) }; + }; + + const { TestScalprumProvider } = mockPluginData({ + module: 'useCounter', + moduleMock: { + default: useCounterMock + } + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByTestId('count')).toHaveTextContent('Count: 0'); + }); + }); +}); +``` + +## Default Values + +### DEFAULT_MODULE_TEST_ID + +Constant for the default test ID used by the default module mock. + +```tsx +import { DEFAULT_MODULE_TEST_ID } from '@scalprum/react-test-utils'; + +expect(screen.getByTestId(DEFAULT_MODULE_TEST_ID)).toBeInTheDocument(); +``` + +Value: `'default-module-test-id'` + +## Jest Setup + +For Jest testing, add initialization to your setup file: + +**jest.setup.js** +```javascript +import { mockScalprum } from '@scalprum/react-test-utils'; + +// Initialize Scalprum mocks globally +mockScalprum(); +``` + +**jest.config.js** +```javascript +module.exports = { + setupFilesAfterEnv: ['/jest.setup.js'], + testEnvironment: 'jsdom', + // ... other config +}; +``` + +## Testing Best Practices + +1. **Call `mockScalprum()` once per test file** - Usually at the top level +2. **Create fresh mocks per test** - Call `mockPluginData()` inside each test +3. **Use async/await with `findBy`** - Remote modules load asynchronously +4. **Mock only what you need** - Don't over-configure `mockPluginData()` +5. **Test error states** - Use `ok: false` to test error handling +6. **Provide API context** - Use second parameter for components needing shared API + +## TypeScript Support + +Full TypeScript support with type definitions: + +```tsx +import { mockPluginData } from '@scalprum/react-test-utils'; +import { PluginManifest } from '@openshift/dynamic-plugin-sdk'; + +interface MyComponentProps { + title: string; + data: number[]; +} + +const manifest: PluginManifest = { + name: 'my-plugin', + version: '1.0.0', + baseURL: 'http://localhost:3001', + loadScripts: ['plugin.js'], + extensions: [], + registrationMethod: 'custom' +}; + +const MyComponentMock: React.ComponentType = ({ title, data }) => ( +
{title}: {data.length} items
+); + +const { TestScalprumProvider } = mockPluginData({ + pluginManifest: manifest, + moduleMock: { + default: MyComponentMock + } +}); +``` + +## Testing Framework Compatibility + +This package is compatible with: + +- **Jest** - Recommended test runner +- **React Testing Library** - For component testing +- **Vitest** - Modern alternative to Jest +- **Any test framework** that supports jsdom environment + +## Package Dependencies + +```json +{ + "dependencies": { + "@openshift/dynamic-plugin-sdk": "^5.0.1", + "@scalprum/core": "^0.8.3", + "@scalprum/react-core": "^0.9.5", + "whatwg-fetch": "^3.6.0" + } +} +``` + +## Troubleshooting + +### Module Not Loading in Tests + +**Problem:** Component shows loading state indefinitely + +**Solution:** Ensure you're using `findBy` queries (async) instead of `getBy`: + +```tsx +// ❌ Wrong - synchronous query +expect(screen.getByTestId('my-component')).toBeInTheDocument(); + +// βœ… Correct - async query +expect(await screen.findByTestId('my-component')).toBeInTheDocument(); +``` + +### Webpack Share Scope Errors + +**Problem:** `__webpack_share_scopes__ is not defined` + +**Solution:** Call `mockScalprum()` before running tests: + +```tsx +import { mockScalprum } from '@scalprum/react-test-utils'; + +mockScalprum(); // Add this + +describe('Tests', () => { + // ... +}); +``` + +### Fetch Not Available + +**Problem:** `fetch is not defined` in test environment + +**Solution:** `mockScalprum()` includes fetch polyfill. If called separately, use: + +```tsx +import { mockFetch } from '@scalprum/react-test-utils'; + +mockFetch(); +``` + +## API Reference + +### Exports + +```typescript +// Main utilities +export function mockScalprum(): void; +export function mockWebpackShareScope(): void; +export function mockFetch(): void; +export function mockPluginData( + options?: MockPluginDataOptions, + api?: ScalprumProviderConfigurableProps['api'] +): { + response: Response; + TestScalprumProvider: React.ComponentType; +}; + +// Constants +export const DEFAULT_MODULE_TEST_ID: string; + +// Re-exports from other packages +export { useScalprum, ScalprumProvider } from '@scalprum/react-core'; +export type { AppsConfig } from '@scalprum/core'; +export type { PluginManifest } from '@openshift/dynamic-plugin-sdk'; +``` + +## Related Packages + +- [`@scalprum/core`](../core) - Framework-agnostic core library +- [`@scalprum/react-core`](../react-core) - React components and hooks +- [`@scalprum/build-utils`](../build-utils) - Build tools and NX executors + +## License + +Apache-2.0