# Packaging a Module for Distribution

https://dev.wisecp.com/es/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

- **A module that runs from a clean copy**: Install into an empty installation first; a hand-added dependency is missing elsewhere.
- **No absolute paths anywhere**: Build paths from the constants.
- **A neutral config file**: Blank every setting, then check the archive.
- **A version number you will not reuse**: The identity installations compare against; decide it before you build.
- **An English language file**: The loader falls back to it for untranslated languages.

## Structure

Everything your module owns lives in one directory named like its class.

```bash
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.

1. `{Name}.php` declares the class; the factory tries `{Name}_Module`, `{Name}`, then the namespaced form.
2. `config.php` returns an array: `status` switches an addon, `meta` carries name, author, icon.
3. `lang/{code}.php` returns a flat array; active language first, English as fallback.
4. `manifest.json` makes the installed copy updatable.

### Archive shape

Zip the full tree from the installation root, only your own directory.

```bash
# 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.

1. The upload is validated: `.zip` or `.tar.gz`, under the size ceiling.
2. It is stored under `temp` and unpacked into a working folder.
3. The `coremio` root makes the whole tree move onto the installation root.
4. The working folder and the archive are deleted either way.
5. An addon can be switched on in the same action, which calls your `enable()`.

```php
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
<?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

- **Extension**: Only `.zip` and `.tar.gz`; extraction uses ZipArchive.
- **Size**: A ceiling of 50 MB, before the host's limits.
- **A coremio directory at the archive root**: The module key comes from the first directory under the type folder.
- **Existing files are overwritten**: Per file: same paths replaced, others left alone.
- **Temp is always cleaned**: On success and on failure, so a failed install leaves nothing.

### 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

```php
// 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;
```

- **AdminTools::extract_archive()**: One folder with a known key, the whole tree without one.
- **Modules::add()**: Where the four fixed filenames are read.
- **Modules::getInstance()**: The canonical way to get an instance; never `new`.
- **AddonModule::change_addon_status()**: The only caller of enable and disable.
- **ModuleUpdater::write_manifest()**: Store path only, so the manifest travels inside your archive.

## Example

A paid addon, packaged: three files decide whether it installs and updates.

```bash
$ 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
<?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'       => '',
    ],
];
```

```json
{
    "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

> **Zipping the folder instead of the tree**
> 
> An archive rooted at the module folder installs through the wizard; the manual upload refuses it.

> **Shipping your test config**
> 
> Nothing blanks it for you. Blank the settings as a build step and check the archive.

> **A missing manifest freezes the copy**
> 
> Nothing fails and the module works, but it is never offered an update again.

> **Shipping only your own language**
> 
> English is the fallback, so a module with only a Turkish file shows blank labels elsewhere.

> **Install your own package first**
> 
> Upload it as a customer would, switch it on, then update from the previous version.

## Related Articles

- [Publishing a Product](https://dev.wisecp.com/en/publishing-a-marketplace-product)
- [Shipping Module Updates](https://dev.wisecp.com/en/shipping-module-updates)
- [Licensing a Module](https://dev.wisecp.com/en/licensing-a-module)
- [Module Anatomy](https://dev.wisecp.com/en/module-anatomy)
- [Module Language Files](https://dev.wisecp.com/en/module-language-files)
- [Module Assets and Logo](https://dev.wisecp.com/en/module-assets-and-logo)
- [Module Lifecycle](https://dev.wisecp.com/en/module-lifecycle)
