The Module System
Understand JopiJS's modular architecture. Sharing, overriding, and extending code.
Overview
JopiJS is not just another framework; it's built around a powerful modular architecture. Instead of a monolithic codebase, your application is a composition of independent blocks called Modules.
These modules act like Lego bricks: they can be added, removed, or even modified without touching their internal code, thanks to JopiJS's unique override system.
1. Why Modules?
The module system solves several common problems in large-scale application development:
- Reusability: Build a "User Auth" module once and drop it into any project.
- Isolation: Each module is a self-contained unit with its own routes, logic, and assets.
- Extensibility: You can modify the behavior of a third-party module without forking it, simply by overriding specific parts of it in your own module.
2. Anatomy of a Module
A module is simply a folder starting with mod_ inside your src directory.
src/
├── mod_core/ # Core features
├── mod_blog/ # A blog module
└── mod_theme/ # Your design systemInside a module, specific folders have special meanings:
| Folder | Purpose |
|---|---|
@routes | Defines the pages and API routes of the application. |
@alias | Public elements (components, classes, functions) shared with other modules. |
package.json | Defines module dependencies (optional). |
3. Sharing Code (@alias)
The @alias folder is how modules talk to each other. Anything placed here works like a public API for your module.
How to share a component
Let's say mod_ui wants to share a Button component.
1. Create the component in @alias/ui
File: src/mod_ui/@alias/ui/Button/index.tsx
export default function Button({ children }) {
return <button className="bg-blue-500 text-white px-4 py-2">{children}</button>;
}2. Use it anywhere
Any file in your project can now import this button using the @ shortcut, which points to the aggregated @alias content of all modules.
import Button from '@/ui/Button';
export default function MyPage() {
return <Button>Click me</Button>;
}4. Overriding & Extending
This is where JopiJS shines. You can replace or extend any public element (from @alias) without touching the original source code.
Scenario: Replacing a Component
Imagine you installed a generic mod_ui, but you want the Button to be red instead of blue. You don't edit mod_ui. Instead, you override it in your own module, say mod_my_theme.
Structure:
src/
├── mod_ui/ # The original module
│ └── @alias/ui/Button/
│ └── index.tsx # The Blue button (default priority)
│
└── mod_my_theme/ # Your custom module
└── @alias/ui/Button/
├── index.tsx # Your Red button
└── high.priority # <--- The magic marker!The Result:
When you write import Button from '@/ui/Button', JopiJS automatically gives you the Red button because of the high.priority marker file. The original Blue button is ignored.
This allows you to highly customize themes or third-party logic without breaking upgradability.
If mod_ui gets an update, your override still works.
Scenario: Extending a Class (Merging)
Sometimes, you don't want to replace a class, but enrich it.
For example, mod_auth defines a User class. A separate mod_profile module might want to add a birthday field to that same user class, without modifying the original code.
JopiJS allows this via Class Merging.
1. The Original Class (mod_auth)
File: src/mod_auth/@alias/lib/User/index.ts
export default class User {
username: string = "Guest";
sayHello() {
return "Hello " + this.username;
}
}2. The Extension (mod_profile)
To add fields to User, we create a file with the same path in another module.
We add a class.merge marker file to tell JopiJS: "Don't replace the original class, merge this one into it".
Structure:
src/
├── mod_auth/ # Original definition
│ └── @alias/lib/User/
│ └── index.ts # class User { username }
│
└── mod_profile/ # Your extension
└── @alias/lib/User/
├── index.ts # class UserExtension { birthday }
└── class.merge # <--- The magic marker!File: src/mod_profile/@alias/lib/User/index.ts
(Plus a class.merge file in the same folder)
export default class UserExtension {
birthday: Date;
// We can even add methods
getAge() {
return new Date().getFullYear() - this.birthday.getFullYear();
}
}The Result:
When you import User anywhere in your app, JopiJS serves you a class that contains both parts.
import User from '@/lib/User';
const u = new User();
u.username = "John"; // From mod_auth
u.sayHello();
u.birthday = new Date(); // From mod_profile
console.log("age:", u.getAge());Scenario: Extending an Interface
Just like classes, you can merge TypeScript interfaces. This is incredibly useful for configuration objects or shared data structures.
1. The Original Interface
File: src/mod_core/@alias/lib/Config/index.ts
export default interface Config {
appName: string;
version: number;
}2. The Extension
We use the interface.merge marker here.
Structure:
src/
├── mod_core/ # Original definition
│ └── @alias/lib/Config/
│ └── index.ts # interface Config { appName, version }
│
└── mod_theme/ # Your extension
└── @alias/lib/Config/
├── index.ts # interface Config { darkMode }
└── interface.merge # <--- The magic marker!File: src/mod_theme/@alias/lib/Config/index.ts
(Plus a interface.merge file in the same folder)
export default interface Config {
// Adding a new property to the Config interface
darkMode: boolean;
}The Result:
The Config type now includes all properties.
import Config from '@/lib/Config';
const myConfig: Config = {
appName: "MyApp",
version: 1,
darkMode: true // Valid! Added by mod_theme
};Summary
- Modules (
mod_*) are the building blocks. @routeshandle URLs listening.@aliasallows sharing between modules.- You can Import shared code with
@/itemType/itemName. - You can Override code by creating a file with the same path and a higher priority.
- You can Extend class (and interface), resulting in a merged class.