Host-contract checks

Paseo's plugin host accepts less than its types suggest. An unknown icon name drops your whole client with one console warning; class syntax works on web and crashes on phones; a module the host does not provide throws only when the plugin loads. ppd check finds these before you install.

npx ppd check            # the plugin in cwd
npx ppd check path/to/plugin --json

It exits 1 on any FAIL; warnings and skips do not fail. All flags are on Commands.

Where the compiler comes from

ppd check compiles and evaluates with Paseo's own code when it can find it, in this order:

  1. --paseo-source or PASEO_SOURCE: a getpaseo/paseo checkout with npm ci done, to test against unreleased Paseo.
  2. The installed Paseo app: --paseo-app, PASEO_APP, the app behind paseo on PATH, /Applications/Paseo.app, ~/Applications/Paseo.app. The compiler and evaluator are read from its app.asar, the Lucide icon list from its web bundle (app-dist).
  3. Neither: an esbuild replica with Paseo 0.11's build options and rules vendored from Paseo. The output says APPROXIMATION on the paseo-compile line and reports the replica's compile as approx-compile. --approx forces this path, which is what CI without Paseo sees.

The checks

CheckWhat it verifies
manifestpaseo-plugin.json follows Paseo's manifest schema: the id format, a requirements.paseo semver range (without it Paseo refuses the plugin), build as argv arrays, a PNG icon that exists inside the package, media paths. Unknown keys are warnings. There must be a client or a server entry.
importsNode modules only on the server, React only on the client. Client code does not import server/, server code does not import client/, shared/ imports neither, and plugin modules live in client/, server/ or shared/.
dependenciesEvery package your code imports is declared, in dependencies when the manifest build omits dev dependencies, and a build step exists for Git installs. Host-provided packages belong in devDependencies. A runtime @types/node of 26 or newer is a warning (it breaks installs on Paseo 0.11.4).
paseo-compilePaseo's own compilePlugin accepts the plugin. Compile errors carry file:line:col.
client-requiresThe client bundle requires only modules the app provides: the SDK, react, react-native, react/jsx-runtime, zod, @tanstack/react-query. Gesture handler, reanimated, safe-area, SVG, Lucide and DOM packages are not available to plugins.
server-requiresThe server bundle requires only host modules and Node built-ins; any other bare require would resolve from Paseo's install, not your plugin.
server-evalThe server contribution runs in a child process and the RPCs it registered are listed: with the app or a checkout, Paseo's own evaluator runs it inside the compile step (120 s limit); on the approximation, a replica runs it with a 30 s limit. The returned cleanup is called; handles it leaves open (servers, timers) are warnings. A process.exit, a hang or a missing cleanup function fails.
client-evalThe client contribution runs in a child process against stub host modules and the app's registration rules: ids, contexts, titles, callbacks, components, duplicates, icons, the cleanup function, and methods the app's client context does not have. Any violation fails, because the app then drops the whole client.
hermes-classNo class syntax in the client bundle: it crashes plugin loading on Hermes, the phone JavaScript engine.
hermes-parsehermesc compiles the client bundle. Skipped without hermesc.
hermes-vmThe Hermes VM evaluates the bundle and runs the contribution. Skipped without the VM.
lucide-iconsEvery contribution icon exists in the app's Lucide set; unknown names in other client source are warnings (the icon renders nothing).
bundle-sizeClient and server sizes against budgets (2 MiB client, 32 MiB server, --max-client-kb and --max-server-kb). Over budget is a warning.

react-native as a devDependency ships both hermesc and the Hermes VM (ppd create adds it); HERMESC and HERMES point at other binaries.

A failing run

The panel template with two typical mistakes: an icon name the app's Lucide build does not have (HandWaving) and a small class in the client.

PASS  manifest         my-plugin, requires paseo >=0.11.4
PASS  imports          client, server and shared boundaries hold
PASS  dependencies     no runtime packages
PASS  paseo-compile    compilePlugin of the Paseo 0.11.4 app (/Applications/Paseo.app) accepted the plugin
PASS  client-requires  client requires only host modules (@getpaseo/plugin, @getpaseo/plugin/client, react, react-native, react/jsx-runtime, zod)
PASS  server-requires  server requires only host modules and Node built-ins
PASS  server-eval      Paseo's evaluator ran the contribution in 0 ms; RPCs: hello.greet
FAIL  client-eval      the app would reject this plugin client and show none of its UI
      Unknown Lucide icon: HandWaving
      contributions: addWorkspacePanel, addCommandCenterItem
FAIL  hermes-class     client bundle contains class syntax, which crashes plugin loading on Hermes (getpaseo/paseo#5783)
      Transforming class syntax to the configured target environment is not supported yet (line 95): var Greeting = class {
FAIL  hermes-parse     hermesc rejected the client bundle
      $TMPDIR/ppd-hermes-0UWV2R/bundle.js:95:16: error: Invalid expression encountered
      var Greeting = class {
                     ^~~~~~~
      Emitted 1 errors. exiting.
FAIL  hermes-vm        Hermes VM failed at evaluate: SyntaxError: 95:16:Invalid expression encountered
FAIL  lucide-icons     unknown Lucide names in contributions (Paseo throws at registration): HandWaving
PASS  bundle-size      client 5 KiB (2 KiB gzip), budget 2.0 MiB; server 2 KiB (1 KiB gzip), eval 0 ms, budget 32.0 MiB

8 passed, 0 warnings, 5 failed, 0 skipped

Paseo itself compiled this plugin without complaint, and on the web the panel would simply not be there. Installing it shows the same verdict from the other side: client-rejected.

In your test suite

// test/host-contract.test.ts: one vitest test per check
import { fileURLToPath } from "node:url";
import { describeHostContract } from "paseo-plugin-devkit/vitest";

describeHostContract(fileURLToPath(new URL("..", import.meta.url)));

Skipped checks show up as skipped tests. Warnings pass unless you pass allowWarnings: false; the other options are the same as ppd check's (paseoApp, paseoSource, maxClientBytes, maxServerBytes).

RPC handlers time out after 30 s in the host, and responses behind the relay share a 32 MiB frame. To keep a handler well inside both:

import { assertWithinRpcBudget } from "paseo-plugin-devkit/testing";

await assertWithinRpcBudget(() => handlers.get("doc.render")!(worstCaseInput)); // < 15 s, < 8 MiB

In CI

Linux runners have no Paseo app, so ppd check runs the approximation there: imports, evaluation, Hermes and sizes still run, the compile is the esbuild replica, and icon names are checked against the plugin's own lucide-react-native if it has one. examples/ci.yml adds a weekly job that checks out Paseo main, sets PASEO_SOURCE and runs the real compiler with the latest SDK.

Limits

Client evaluation and the Hermes run use stub host modules, not the real app: they catch load-time failures, not rendering bugs. Use ppd web and a phone for those. Time limits are 30 s per evaluated bundle and 120 s for the real compile with the server evaluation inside it. The CLI has no flag for them; runChecks and describeHostContract take evalTimeoutMs and compileTimeoutMs.