Plugin format
A plugin is a folder with a plugin.json and an ES module for a tab in the UI, Dart for the app, or both. An app copies the folder into lib/devpul/<id>/, records it in devpul.lock and calls DevpulPlugins.install() from the generated lib/devpul/plugins.dart, which offers every plugin to the UI with Devpul.plugin in debug builds.
Plugin folder
Section titled “Plugin folder”cart/ plugin.json cart.js ES module the UI runs cart.dart Dart the app calls icon.svg tab icon, optional README.md example/ optional, never copieddevpul create cart writes this layout with a pubspec.yaml for working on the Dart in place and devpul.d.ts with the plugin API types; neither is copied. devpul check validates a folder the way devpul add and the registry do.
plugin.json
Section titled “plugin.json”{ "id": "cart", "version": "1.2.0", "api": 1, "devpul": "^1.0.0", "description": "Cart contents and totals in a Cart tab.", "tags": ["shop", "state"], "icon": "icon.svg", "readme": "README.md", "usage": ["DevpulCart.changed(items, total);"], "requires": { "collection": "^1.18.0" }, "source": "cart.js", "dart": "cart.dart", "files": ["cart.dart", "cart.js", "icon.svg"]}| Field | Required | Value |
|---|---|---|
id |
yes | A lowercase letter, then up to 31 lowercase letters, digits or _. |
version |
yes | A semantic version, such as 1.2.0 or 2.0.0-dev.1. |
api |
with source |
1, the plugin API the module is written for. |
devpul |
yes | A pub version constraint on the devpul package the app uses. |
description |
yes | One line, up to 160 characters: what the plugin shows. In the marketplace and the UI's Plugins tab. |
tags |
Up to 5, each up to 20 lowercase letters, digits or -. |
|
icon |
An SVG file in files, shown as the tab icon. Only with source. |
|
readme |
A file in the folder, shown in the registry. Not copied unless it is in files. |
|
usage |
1 to 3 lines of wiring the app adds by hand, up to 100 characters each. | |
requires |
Packages the Dart imports besides devpul, with pub version constraints. The app depends on them itself. |
|
source |
The ES module, a .js file in files, up to 512 KB with the icon. A plugin has source, dart or both. |
|
dart |
A .dart library in files, exported by the generated file. |
|
files |
yes | Every file copied into the app besides plugin.json: relative paths with /, no . or .. segments and no hidden files. |
Other fields are rejected.
The id names the folder, the Devpul.plugin id the UI keeps approvals for, and the plugin's git tags: <id>-v<version>, such as cart-v1.2.0.
Module
Section titled “Module”The module follows the plugin API: one file with a default export and no imports. It does not export a name icon: the generated file appends export const icon = "<svg ...>" with the icon file, and the UI uses it for each tab that sets no icon.
The generated file exports every plugin's dart library into one namespace, so public names start with Devpul<Id> or devpul<Id>, such as DevpulCart. The Dart imports package:devpul, the packages in requires and its own files in files.
A plugin with only Dart adds no tab. It sends events to the built-in tabs, as an adapter for another HTTP client would for Network, and its events show in Events.
Code the app calls in every build, such as an observer in main, does nothing unless Devpul.included, a constant, so the compiler drops the plugin from release builds and from profile builds that do not opt in.
In the app
Section titled “In the app”devpul.locklib/devpul/ plugins.dart generated cart/ copied, committed plugin.json cart.dart cart.js icon.svgEverything is committed, so plugin code shows up in the app's diffs and builds need no network or cache.
import 'devpul/plugins.dart';
void main() { DevpulPlugins.install(); runApp(const App());}devpul.lock
Section titled “devpul.lock”{ "format": 1, "plugins": { "cart": { "url": "https://example.com/plugins.git", "path": "cart", "constraint": "^1.2.0", "commit": "0123456789abcdef0123456789abcdef01234567", "version": "1.2.0", "hash": "abaf05812a3ec7f4b4f17a0d801d5175808b3b6a5847e3fcbabc032641328f4e" }, "flags": { "url": "https://example.com/flags/plugin.json", "version": "0.3.1", "hash": "9adea98c93e0d8a2d88dfff7a850d1dd6764b3d1530699657bedaba0f9d96a4a" } }}| Field | |
|---|---|
format |
1. |
plugins |
By id, sorted. |
url |
A git repository, a plugin.json URL, or a folder path relative to the app. |
path |
The plugin folder inside a git repository. |
ref |
The git ref the plugin is pinned to. |
constraint |
The versions an update may move to, read from the <id>-v<version> tags. |
commit |
The resolved git commit. |
version |
The copied version. |
hash |
SHA-256 of the copied folder: sha256sum lines of its files sorted by path, CRLF read as LF. |
The hash can be checked by hand:
cd lib/devpul/cartfind . -type f | sed 's|^\./||' | LC_ALL=C sort | xargs sha256sum | sha256sumlib/devpul/plugins.dart
Section titled “lib/devpul/plugins.dart”// Generated by devpul from devpul.lock. Do not edit.//// cart 1.2.0 from https://example.com/plugins.git// DevpulCart.changed(items, total);
// dart format off// ignore_for_file: type=lint
import 'package:devpul/devpul.dart';
export 'cart/cart.dart';
abstract final class DevpulPlugins { static void install() { if (!Devpul.included) return; Devpul.plugin( 'cart', () => r'''export default { ... };export const icon = "<svg ...>";''', version: '1.2.0', origin: 'https://example.com/plugins.git', description: 'Cart contents and totals in a Cart tab.', ); }}The file is written whole from devpul.lock and the copied files, so it is never edited. Each plugin's usage is in the header. A plugin with no source is exported and not offered. The handshake lists origin, the lock's url, and the manifest's description.
- Not named
.g.dart, so ignore rules for build_runner output leave it committed. // dart format offkeeps it the same under any formatter settings.Devpul.includedis false whendart.vm.productis set, and whendart.vm.profileis set withoutDEVPUL_PROFILEor on the web. Those are the constants behind Flutter'skReleaseModeandkProfileMode, so release builds and profile builds that do not opt in drop every plugin on every platform, and so doesdart compile exe.dart compile jssets neither; pass-Ddart.vm.product=true.
registry.json
Section titled “registry.json”Served at https://devpul.saradgajurel.com.np/plugins/registry.json, next to the plugins page. One entry per plugin at its latest version. url and path are what a lock entry records for a plugin installed from it. Check for updates in the UI's Plugins tab matches an entry by id and by url against the handshake origin.
The registry is built from the <id>-v<version> tags of the plugins in the DevPul repository and the entries in its registry/third-party.json, each fetched as devpul add <id> would fetch it and checked as devpul check checks a folder. A third-party entry is an id and an https url: a git repository with <id>-v<version> tags, plus path for the plugin folder in it, or a plugin.json URL. The README is shown on the plugin's page as markdown without HTML; images show as links.
An entry with package is a pub package listed under a plugin id, from registry/packages.json (id, package, tags, usage) at its latest version on pub.dev, with the pubspec's description. It has no api, devpul, requires, icon or path, and url is its pub.dev page. devpul add <id> runs pub add for it; the UI skips it in update checks.
{ "format": 1, "plugins": [ { "id": "cart", "version": "1.3.0", "api": 1, "devpul": "^1.0.0", "description": "Cart contents and totals in a Cart tab.", "tags": ["shop", "state"], "icon": "<svg ...>", "usage": ["DevpulCart.changed(items, total);"], "requires": {}, "url": "https://example.com/plugins.git", "path": "cart" }, { "id": "net", "version": "2.1.0", "description": "Requests from the net client in the Network tab.", "tags": ["network"], "usage": ["final client = NetClient(devpul: true);"], "package": "example_net", "url": "https://pub.dev/packages/example_net" } ]}