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.

Project structure
src/
├── mod_core/            # Core features
├── mod_blog/            # A blog module
└── mod_theme/           # Your design system

Inside a module, specific folders have special meanings:

FolderPurpose
@routesDefines the pages and API routes of the application.
@aliasPublic elements (components, classes, functions) shared with other modules.
package.jsonDefines 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.
  • @routes handle URLs listening.
  • @alias allows 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.