Skip to main content

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-m metadata in frontend/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