Package Frontend Structure Guide
Overview
Framework M supports multi-package UI composition, allowing Python packages to contribute frontend code that is automatically discovered and bundled during build or development.
Package Structure
A Framework M package with frontend components follows this structure:
my-package/
├── pyproject.toml # Package configuration
├── README.md
├── frontend/ # Frontend source code (TypeScript, React)
│ ├── index.ts # Plugin entry point (required)
│ ├── package.json # Frontend dependencies & metadata
│ ├── tsconfig.json # TypeScript config
│ ├── vite.config.mfe.ts # MFE build config (outputs to src/static/mfe)
│ ├── components/ # Shared components
│ └── pages/ # Custom pages
└── src/
└── my_package/ # Python package namespace
├── __init__.py
├── doctypes/ # Backend DocTypes
└── static/ # Built assets (included in wheel, gitignored)
└── mfe/
└── remoteEntry.js
Required Files
1. pyproject.toml
Register both backend and frontend entry points:
[project]
name = "my-package"
version = "1.0.0"
dependencies = [
"framework-m>=0.1.0",
]
# Backend entry point (for DocType discovery)
[project.entry-points."framework_m.apps"]
my_package = "my_package:app"
# Frontend marker (for MFE discovery)
# Points to the module containing the 'static/mfe' directory
[project.entry-points."framework_m.frontend"]
my_package = "my_package.frontend:plugin"
[tool.hatch.build.targets.wheel.force-include]
"src/my_package/static" = "my_package/static"
2. frontend/src/plugin.config.ts
The main plugin entry point that defines your UI components and routes:
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
import { MyCard } from "./components/MyCard";
import { MyForm } from "./components/MyForm";
import { MyListPage } from "./pages/MyList";
import { MyDetailPage } from "./pages/MyDetail";
// Define and export the plugin manifest
export const plugin: FrameworkMPlugin = {
name: "my_package",
version: "1.0.0",
// Register custom pages
routes: [
{
path: "/app/my-doctype/list",
element: MyListPage,
},
{
path: "/app/my-doctype/:id",
element: MyDetailPage,
},
],
// Register components for use by other plugins
components: {
MyCard,
MyForm,
},
};
export default plugin;
Development Workflow
Local Development
Install your package in development mode and run the dev server:
# Install as editable package
pip install -e .
# Run dev server (auto-discovers all plugins in workspace)
m dev
# Your plugin UI is now available at http://localhost:5173
The dev server will:
- Discover your package via the
framework-mmetadata infrontend/package.json - Generate Vite config with path aliases
- Enable Hot Module Replacement (HMR) for your TypeScript/React code
Building for Production
For production, you build your frontend as an MFE remote:
cd frontend
pnpm build:mfe
# Result: Optimized assets in src/my_package/static/mfe/
Frontend Dependencies
package.json
Each package can declare its own frontend dependencies:
{
"name": "@my-package/frontend",
"version": "1.0.0",
"framework-m": {
"plugin": "./src/plugin.config.ts",
"type": "frontend-module"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@framework-m/plugin-sdk": "^0.1.0"
},
"dependencies": {
"date-fns": "^2.30.0",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"typescript": "^5.0.0"
}
}
Important: Each frontend plugin must include the framework-m metadata block in its package.json to be discovered by the build system.
TypeScript Configuration
Create a tsconfig.json for type checking:
{
"extends": "@framework-m/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./",
"baseUrl": ".",
"paths": {
"@framework-m/core": ["../../framework-m/frontend/src"],
"@/*": ["./src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
Import Patterns
Importing from Framework M SDK
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
import { usePluginMenu } from "@framework-m/plugin-sdk";
Importing UI Components
import { StateBadge, ThemeProvider } from "@framework-m/ui";
Importing from Other Packages
// Import components from business-m package (Source aliases work in Monorepo)
import { CustomerCard } from "@business-m/frontend/components/CustomerCard";
The build system automatically resolves these imports via Vite path aliases.
Best Practices
1. Component Isolation
Keep components self-contained with their own styles:
// components/MyCard.tsx
import { Card } from "@framework-m/core";
import styles from "./MyCard.module.css";
export function MyCard({ data }) {
return (
<Card className={styles.card}>
{/* Component content */}
</Card>
);
}
2. Type Safety
Export TypeScript types for components consumed by other packages:
// types/index.ts
export interface MyCardProps {
data: MyData;
onAction?: (id: string) => void;
}
// components/MyCard.tsx
import type { MyCardProps } from "../types";
export function MyCard({ data, onAction }: MyCardProps) {
// Implementation
}
3. Tree-Shaking
Use named exports to enable tree-shaking:
// ✅ Good: Named exports
export { MyCard } from "./components/MyCard";
export { MyForm } from "./components/MyForm";
// ❌ Bad: Default exports
export default {
MyCard,
MyForm,
};
4. Code Splitting
Use dynamic imports for large components:
import { lazy } from "react";
const LargeReport = lazy(() => import("./components/LargeReport"));
registerPlugin({
name: "my_package",
pages: [
{
path: "/app/reports/large",
component: LargeReport, // Auto-code-split
},
],
});
Testing
Unit Tests
Test components in isolation:
// components/__tests__/MyCard.test.tsx
import { render } from "@testing-library/react";
import { MyCard } from "../MyCard";
describe("MyCard", () => {
it("renders data correctly", () => {
const { getByText } = render(<MyCard data={{ title: "Test" }} />);
expect(getByText("Test")).toBeInTheDocument();
});
});
Unit Tests
Test components in isolation using standard Vitest/React Testing Library setup in the frontend/ directory.
Integration Tests
Test your plugin manifest and service registration:
// frontend/src/__tests__/plugin.test.ts
import { PluginRegistry } from "@framework-m/plugin-sdk";
import { plugin } from "../plugin.config";
describe("Plugin Registration", () => {
it("registers with correct metadata", async () => {
const registry = new PluginRegistry();
await registry.register(plugin);
const registered = registry.getPlugins();
expect(registered[0].name).toBe("my_package");
});
});
Publishing
1. Build the MFE Remote
cd frontend
pnpm build:mfe
2. Verify Built Assets
Ensure static/mfe/remoteEntry.js exists in your Python source:
ls src/my_package/static/mfe/remoteEntry.js
3. Build the Python Wheel
# Hatchling uses force-include to bundle the static assets
python -m build
4. Verify Wheel Contents
tar -tzf dist/my_package-1.0.0.tar.gz | grep static/mfe
# Should show the remoteEntry.js and assets inside the Python package
Next Steps
- Plugin Composition Patterns - Learn how to compose and override UI from multiple packages
- Plugin-Host Composition Patterns - Reference examples and CI/CD templates
- Multi-Package UI Composition Architecture - Progressive deployment strategy
- Example: business-m Package - Complete working example