Packaging a Module for Distribution
Turn a module directory into an archive somebody else can install: what belongs in it and what must never be.
Overview
A package is one directory, zipped, with its path preserved. Two consumers read it differently.
| Consumer | Accepts | Result |
|---|---|---|
| Manual upload | The full tree: coremio at the root | Merged into the installation root, file by file |
| The update wizard | The module folder, the tree, or an unnamed folder | Into the module's directory, after a rollback copy |
| A store download | The module folder at the root | Into the module directory, then a manifest |
One shape satisfies all three: ship the full tree, rooted at coremio/modules/{Type}/{Name}/.
Prerequisites
Structure
Everything your module owns lives in one directory named like its class.
AuroraBackup-1.2.0.zip
└─ coremio/
└─ modules/
└─ Addons/
└─ AuroraBackup/
├─ AuroraBackup.php # the class; the loader looks for this exact name
├─ config.php # settings and meta; skipped by an update if present
├─ manifest.json # enrols the copy in the update check
├─ hooks.php # optional, loaded by the hook system
├─ router.php # optional, registers an admin page
├─ AdminArea.php # optional, the admin page itself
├─ logo.png # optional, picked up by name
├─ lang/
│ ├─ en.php # the fallback, always ship it
│ └─ tr.php
├─ views/
│ └─ index.php
└─ src/
└─ ApiClient.php # your own helper classes
Rename the directory and the class lookup, update detection and logo all stop working.
Walkthrough
Lay out the directory
Four files are read from fixed places.
{Name}.phpdeclares the class; the factory tries{Name}_Module,{Name}, then the namespaced form.config.phpreturns an array:statusswitches an addon,metacarries name, author, icon.lang/{code}.phpreturns a flat array; active language first, English as fallback.manifest.jsonmakes the installed copy updatable.
Archive shape
Zip the full tree from the installation root, only your own directory.
# From the root of a clean installation, so the stored paths are root relative.
zip -r AuroraBackup-1.2.0.zip coremio/modules/Addons/AuroraBackup \
-x '*/.git/*' '*/.DS_Store' '*/node_modules/*' '*.log' '*.map'
# Read the archive back and look at it. This is the only way to see what you
# actually shipped rather than what you meant to ship.
unzip -l AuroraBackup-1.2.0.zip
A zip inside the zip is unpacked once and matches nothing.
Strip what must not ship
Leftovers bloat the package; credentials and stray files damage the target.
Read the built archive, not the working directory.
What happens on the target system
The operator uploads from the panel; what follows is fixed.
- The upload is validated:
.zipor.tar.gz, under the size ceiling. - It is stored under
tempand unpacked into a working folder. - The
coremioroot makes the whole tree move onto the installation root. - The working folder and the archive are deleted either way.
- An addon can be switched on in the same action, which calls your
enable().
public function enable(): bool
{
// Everything the module needs in order to exist: its tables, its notification
// templates, its seed rows. Idempotent, because an operator can switch a module
// off and on again as often as they like.
$this->check_database();
// Column repair runs after table creation, never inside it: a seed that writes a
// column added later would die on an installation whose table predates it.
$this->check_columns();
// Returning false aborts the switch-on and leaves the module disabled, so use it
// for a genuine blocker and never for a warning.
return true;
}
No manifest is written, and no enable() runs for types with no switch: a server or registrar module creates what it needs elsewhere.
Verify before publishing
Unpack the built archive into an empty installation and read it back.
<?php
require dirname(__DIR__) . '/bootstrap.php';
$type = 'Addons';
$key = 'AuroraBackup';
$dir = MODULE_DIR . $type . DS . $key;
$fail = [];
// 1. The four files the platform reads from fixed places.
foreach ([$key . '.php', 'config.php', 'manifest.json', 'lang' . DS . 'en.php'] as $file)
if (!is_file($dir . DS . $file)) $fail[] = 'missing: ' . $file;
// 2. The loader's own answer, not our guess at it.
Modules::Load($type, $key, true);
$config = Modules::Config($type, $key);
if (!is_array($config)) $fail[] = 'config.php did not return an array';
// 3. Credentials that survived the build. Read the SHIPPED settings, not your notes.
foreach ((array) ($config['settings'] ?? []) as $name => $value)
if (is_string($value) && $value !== '') $fail[] = 'setting not blank: ' . $name;
// 4. Update enrolment.
if (!ModuleUpdater::manifest($dir)) $fail[] = 'manifest.json unreadable: no updates would ever be offered';
echo $fail ? implode("\n", $fail) . "\n" : "package looks shippable\n";
Reference
What the platform reads
| File | Read by | Without it |
|---|---|---|
{Name}.php | the loader; the wizard's proof | Nothing matches, nothing can be instantiated |
config.php | the loader, on every registry build | No settings, no status: loaded but inert |
lang/en.php | the loader, as the fallback | Labels resolve to nothing in untranslated languages |
lang/{code}.php | the loader, on the active language | That language falls back to English |
manifest.json | the daily update check | The copy reads as hand installed, never updated |
logo.* | the logo resolver, by glob | No logo, unless the config names one |
hooks.php | the hook system, at boot | Nothing registers |
Upload limits
.zip and .tar.gz; extraction uses ZipArchive.
What must never be in the archive
| Do not ship | Why |
|---|---|
Anything outside coremio/modules/{Type}/{Name}/ | It replaces a core file, silently |
Your API keys and test accounts in config.php | The settings array ships verbatim |
| A licence public key you have not rotated | It is what makes a forged answer worthless |
| Version control and editor directories | History, branch names, credentials still in the log |
| Dependency and build directories, source maps | A source map undoes the build |
| Development scratch files and probe scripts | A bootstrapping script becomes reachable on their server |
| Logs, dumps, backups and storage output | Somebody else's data, stale and possibly personal |
| A second archive inside the archive | Unpacking happens once, so nothing matches |
Entry points
// The archive handler both upload paths share. $name empty means "work the key out
// from the tree", which is why the manual path needs the full tree.
public function extract_archive($file = '', $name = '', $group = 'Addons'): string;
// Registry build: reads config.php, then lang/{active}.php with lang/en.php as the
// fallback, then includes {Name}.php unless the class is already declared.
public static function add($file, $type, $nominc = false, $status = '');
public static function Load($type = '', $name = '', $nominc = false, $status = '');
// getInstance takes the FIRST of these three class names that exists, in this order:
// {Name}_Module -> {Name} -> WISECP\Modules\{Ucfirst Type}\{Name}
// Missing constructor arguments are padded with null, so a required parameter your
// class cannot cope with as null is a crash at instantiation, not a helpful error.
public static function getInstance(string $type, string $name, array $params = []): ?object;
// Switching an addon on. Calls activate() then enable() and writes the flag only if
// the last one returned true.
public function change_addon_status($arg = '');
// Written by the store install path, and by you inside the package for every other route.
public static function write_manifest(string $dir, array $manifest): bool;
new.
Example
A paid addon, packaged: three files decide whether it installs and updates.
$ unzip -l AuroraBackup-1.2.0.zip
coremio/modules/Addons/AuroraBackup/AuroraBackup.php
coremio/modules/Addons/AuroraBackup/config.php
coremio/modules/Addons/AuroraBackup/manifest.json
coremio/modules/Addons/AuroraBackup/hooks.php
coremio/modules/Addons/AuroraBackup/logo.png
coremio/modules/Addons/AuroraBackup/lang/en.php
coremio/modules/Addons/AuroraBackup/lang/tr.php
coremio/modules/Addons/AuroraBackup/views/index.php
coremio/modules/Addons/AuroraBackup/views/unlicensed.php
coremio/modules/Addons/AuroraBackup/src/ApiClient.php
# Nothing above coremio/modules/Addons/AuroraBackup. No .git, no node_modules,
# no temp scripts, no second archive, no storage output.
<?php
return [
'created_at' => 1785312000,
'meta' => [
'name' => 'Aurora Backup',
'version' => '1.2.0',
'author' => 'Aurora Systems',
'opening-type' => 'normal',
'icon_type' => 'font',
'icon' => 'bi bi-cloud-arrow-up',
'slug' => 'aurora-backup',
],
'show_on_adminArea' => true,
'show_on_clientArea' => false,
// Ships disabled: the operator switches it on, and that is what calls enable().
'status' => false,
'access_ps' => [],
// Every value blank. This array is written back verbatim from your disk.
'settings' => [
'api_endpoint' => '',
'api_key' => '',
'bucket' => '',
],
];
{
"type": "marketplace",
"id": 42,
"version": "1.2.0",
"last_updated": "2026-08-03"
}
The version appears twice: config is what the operator reads, the manifest is what updates compare against.
Pitfalls
An archive rooted at the module folder installs through the wizard; the manual upload refuses it.
Nothing blanks it for you. Blank the settings as a build step and check the archive.
Nothing fails and the module works, but it is never offered an update again.
English is the fallback, so a module with only a Turkish file shows blank labels elsewhere.
Upload it as a customer would, switch it on, then update from the previous version.
Related Articles
Grazie per il tuo feedback!
Il nostro team di assistenza è disponibile 24 ore su 24 per aiutarti a trovare le risposte che cerchi.