A plugin is described by two files at the module root: module.json (read by nwidart and by the Plugin Manager) and composer.json (read by Composer's merge plugin and checked on upload).
module.jsonModules/Stripe/module.json (a real, minimal manifest)
{
"name": "Stripe",
"alias": "stripe",
"description": "Adds Stripe payment processing to TruPayments ...",
"version": "1.0.0",
"author": "DotArtisan",
"keywords": ["payments", "stripe", "gateway", "trupayments"],
"priority": 1003,
"requires": { "trupayments": "^1.0" },
"providers": ["Modules\\Stripe\\Providers\\StripeServiceProvider"]
}| Key | Type | Rule (enforced by PluginMetadataReader) | Error key |
|---|---|---|---|
name | string | StudlyCase, ^[A-Z][A-Za-z0-9]{1,63}$. Must equal the folder name and the namespace segment Modules\{name}\ | name_invalid |
alias | string | Lower-case kebab, ^[a-z][a-z0-9-]{1,63}$. Used as the config key, view namespace, translation namespace and in other plugins' requires | alias_invalid |
description | string | Non-empty; shown in the Plugin Manager | description_missing |
version | string | Anything Composer's VersionParser::normalize() accepts (1.0.0, v2.1, 1.0.0-beta1). Compared against other plugins' requires constraints | version_invalid |
providers | list<string> | At least one fully-qualified class, each matching ^Modules\\{Name}\\[A-Za-z0-9_\\]+$; the file app/{path}.php must exist in the archive | providers_missing, providers_invalid, provider_invalid, provider_missing |
| Key | Type | Meaning |
|---|---|---|
requires | map<alias, constraint> or list<alias> | Plugins that must be installed and active, with Composer constraints ("trupayments": "^1.0"). A list means any version. Aliases must match the alias rule; constraints must parse. The legacy key requirements (list) is merged in. There is no key for the TruCMS core version |
public_paths | list<string> | First URL segments your public routes own (["booking"]). They are added to the reserved-slug list so a CMS page can never be created at /booking, and removed from the catch-all route's pattern. Each must match ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$. The lower-case module name is always reserved too |
protected | bool | true forbids deleting the plugin from the UI (non-boolean values are rejected: protected_invalid) |
author | string or {name, url} | Displayed in the manager |
author_url (alias website), support_url (alias support) | http(s) URL | Links in the manager; other schemes are rejected (url_invalid) |
payments | bool | true marks a payment consumer; Module::paymentModules() / withPayment() list them (Booking, TruCommerce) |
priority | int | nwidart sorts modules by it (ascending) before calling register() and boot(). Bundled values: TruCommerce 99, TruPayments 100, Booking 1000, gateways 1002–1004. Use a high number so you load after the modules you extend |
keywords | list<string> | Informational |
files, aliases | nwidart keys | files are required early by nwidart (registerFiles()); aliases registers class aliases |
Unknown keys are ignored, so you can store your own metadata, and they are readable with Module::find('Name')->get('key').
composer.jsonModules/Booking/composer.json (trimmed)
{
"name": "dotartisan/booking",
"require": { "php": "^8.3", "astrotomic/laravel-translatable": "^11.16" },
"autoload": {
"files": ["booking.php"],
"psr-4": {
"Modules\\Booking\\": "app/",
"Modules\\Booking\\Database\\Factories\\": "database/factories/",
"Modules\\Booking\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": { "psr-4": { "Modules\\Booking\\Tests\\": "tests/" } }
}autoload.psr-4["Modules\\{Name}\\"] is exactly app (or app/). Otherwise: composer_json_missing, composer_json_invalid, autoload_invalid.autoload.files is how a module ships global helper functions (Booking's booking.php).composer.json contains "extra": {"merge-plugin": {"include": ["Modules/*/composer.json"]}}, so each module's autoload block is merged when you run composer dump-autoload / install / update.require entries in a module's composer.json are merged as well; a plugin that needs an extra PHP library must therefore be installed with Composer access (the Plugin Manager upload does not run Composer). Prefer dependency-free plugins for ZIP distribution, or document the Composer step.name can be anything; it is not used by TruCMS.config/modules.php defines where nwidart expects files. The ones that matter: providers and route provider in app/Providers, controllers app/Http/Controllers, models app/Models, config config (every PHP file is merged: config.php → config('alias')), migrations database/migrations, seeders database/seeders, translations lang, views resources/views, assets resources/assets (published to public/modules/{lowername} by php artisan module:publish Name), routes routes, tests tests/Feature and tests/Unit.
use Nwidart\Modules\Facades\Module;
Module::isEnabled('Testimonials'); // bool, reads modules_statuses.json
Module::find('Testimonials')?->get('version'); // any manifest key
Module::allEnabled(); // enabled modules
Module::requirements('booking'); // ['trupayments'] (macro added by TruCMS)
Module::requiredBy('trupayments'); // collection of modules that require it
module_path('Testimonials', 'resources/views'); // helper: absolute path inside a module| Message shown | Fix |
|---|---|
| The package must contain exactly one module.json file at the module root | Put module.json in the top folder of the ZIP (or the ZIP root) and ensure no nested copies exist. |
| The declared provider … does not match the module namespace | Providers must start with Modules\{Name}\; the folder/alias must agree with name. |
| The declared provider file for … is missing | The class path maps to app/Providers/YourServiceProvider.php (everything after Modules\{Name}\ is a path inside app/). |
| composer.json must map … to the module app directory | Add "Modules\\{Name}\\": "app/" to autoload.psr-4. |
| Plugin dependencies must be a map of plugin aliases to semantic-version constraints | Use {"trupayments": "^1.0"}, not a string. |
| … is already installed. Plugin updates are not supported by this upload flow | See Package, install and update. |
| The package must contain a single module directory | Do not zip several folders (macOS __MACOSX folders count). |