ubd create scaffolds a plugin that already meets them. Most rules are checked by a lint rule from @unbound-app/cli/oxlint or by a ubd build check, and each rule below names its check.
Summary
No eager lookups while the plugin loads
A plugin’s bundle is evaluated each time the plugin starts. Code at the top of a file runs during that evaluation, beforestart, and at launch it delays Discord. A Metro search walks every module in Discord’s bundle.
So code that runs while the plugin loads makes no eager search: no metro.find, no findBy* or flux.getStore with { lazy: false }. The findBy* helpers and flux.getStore are lazy by default. They return a proxy, and the search runs the first time the plugin reads from it.
unbound/no-module-scope-lookup. It reports a metro.find, findByProps, findByName, findByFilePath, findByPrototypes or flux.getStore call made outside a function, unless the call is lazy. The scaffold turns it on with { "lazyByDefault": true }, so only a call with { lazy: false } is reported.
Each feature loads itself and patches only what it needs
A feature is one thing the plugin does, in its own domain folder.src/index.tsx starts each feature. The feature then finds its own modules and makes its own patches. A feature may be always on. It doesn’t need a setting.
A feature makes only the patches it needs. One exception is a patch on another module that exists only to get data to the feature, such as passing on data from a closure. That patch belongs to the feature that uses the data, in that feature’s folder.
Patch lazy targets. If the module hasn’t loaded yet, the patcher waits and applies the patch when it loads, so a patch never makes Discord load a module early.
src/greeting/index.ts
unbound/no-module-scope-lookup for the lookups a feature makes. No lint rule or build check can tell whether a patch is needed, so addon review checks that.
The standard layout
src/index.tsxonly starts features and exports the settings panel.- The only files at the root of
src/areindex.tsx,settings.tsx,constants.tsandtypes.ts. Every other file goes in a domain folder. - Platform code goes in the
ios/andandroid/folders of the domain it belongs to, never at the root ofsrc/. - A domain’s own components go in that domain’s folder.
layout. ubd build prints one warning per departure: a missing src/index.tsx, another file at the root of src/, ios/ or android/ at the root of src/, or a domain with ios/ or android/ but no platform.ts. Warnings don’t fail the build.
platform.ts is the only platform check
A domain with platform code picks its platform handler once, in platform.ts, with native.definePlatform. The rest of the domain calls the handler and never checks the platform itself.
src/greeting/platform.ts
unbound/platform-check-only-in-platform-file, which reports Platform.OS and context.platform in any file other than platform.ts, and by the build check layout, which warns about an ios/ or android/ folder with no platform.ts next to it. ubd build also reads the keys you pass to definePlatform to know which platforms the plugin supports.
Use the scoped API, never window.unbound
Import namespaces from @unbound-app/api. In a plugin they come from its Addon scope, so the plugin’s patches, commands, timers and listeners are removed when it stops. window.unbound is the unscoped API: what a plugin registers through it outlives the plugin.
Enforced by the lint rule unbound/no-window-unbound, which reports window.unbound and globalThis.unbound, including inside strings.
Developer notes are JSDoc on the feature
Notes for the next developer go in the JSDoc of the feature’sindex.ts, next to the code they describe. Name every patch the feature forces on another module, and every Discord module or store it depends on.
Enforced by addon review. No lint rule or build check reads the notes.
Never write Capabilities or platforms by hand
ubd build writes platforms and each platform’s Capabilities into dist/manifest.json. It reads them from the native calls each platform handler can reach. A plugin never writes them in its manifest.json.
Enforced by the build check capabilities. A hand-written capabilities, platforms or platformCapabilities field, or a dynamic native.objc[name] access, is an error and fails the build.
What ubd build and the lint rules check
ubd build runs these checks after it bundles a plugin. An error fails the build, and a warning is printed.
A scaffolded plugin’s
.oxlintrc.json turns on the three lint rules:
.oxlintrc.json