Skip to content

Guide to Writing Yeow Dependency Packages

Yeow plugins can encapsulate shared logic and resources as npm packages for other plugins to reuse. Typical scenarios:

  • Encapsulating Services (three package types: SDK / JS service / native service, see "Encapsulating Service Packages (Three Types)" below)
  • Encapsulating Native Services (e.g., yeow-image packages image-svc.exe + registration/invocation logic)
  • Shared utility functions (e.g., yeow-command, yeow-server)
  • Shared configuration templates, resource files

Package Structure

yeow-image/
├ package.json          ← Package metadata (key fields see below)
├ tsconfig.json         ← TS configuration (optional)
├ assets/               ← Resources distributed with package (exe, configs, images, etc.)
│  └ image-svc.exe
└ src/
   └ index.ts          ← Package entry (main/types points here)

package.json

json
{
    "name": "yeow-image",
    "version": "0.1.0",
    "type": "module",
    "main": "./src/index.ts",
    "types": "./src/index.ts",
    "files": ["src/", "assets/"],
    "peerDependencies": {
        "yeow-api": "^0.4.0"
    },
    "devDependencies": {
        "yeow-api": "^0.4.0"
    },
    "license": "MIT"
}

Key Fields

FieldDescription
main / typesPoints to src/index.ts source code (built by main project esbuild directly bundling source, no pre-compilation needed)
filesMust include assets/, otherwise resources won't be distributed with package when published to npm
peerDependenciesContract + build-time identification marker: yeow-api version range. Declares "consumer plugin must install yeow-api"; builder also uses this as identification condition (combined with assets/) to decide whether to package this package's resources into JAR and merge permissions (see "Dependency Identification" below)
devDependenciesDevelopment dependencies: Independent development import 'yeow-api' needs type definitions and IDE hints
bash
npm install --save-dev yeow-api

Division of Two Declarations

DeclarationScopePurpose
peerDependenciesConsumer side (npm install + build-time)Contract: Consumer's plugin must have yeow-api. When ranges don't overlap, npm installs independent yeow-api副本 for package — multiple copies can safely coexist (see "Build-time Automatic Handling" below). Also serves as builder's dependency identification marker — only packages with assets/ and peer containing yeow-api participate in asset packaging and permission merging (missing declaration → resources silently not included in JAR)
devDependenciesDevelopment time (author side)Type/hints during independent development; not published with package, doesn't affect runtime

Version Strategy (two ranges, each with its own role, don't need to be same):

  • devDependencies = Development target version — version used during development/type checking, can be narrow
  • peerDependencies = Widest compatible range — npm 7+ installs separate yeow-api copy for consumer when ranges don't overlap (increases bundle size); wide range avoids duplicate installation. Old packages using only APIs common to all versions can write ^0.3.0 || ^0.4.0; dependency package source code is checked by consumer's tsc --noEmit (build-time typecheck), as long as package only uses APIs available in all versions within range, wide range is naturally safe. Even version incompatibility doesn't affect runtime: Multiple yeow-api copies share lifecycle/event/GC global registry, can safely coexist (see below)

Permission Declaration

Sensitive message nodes are denied by default, must be declared by main project depending on this package in yeow.config.json's permissions field (automatically merged and computed into computedPermissions during build):

Nodes Requiring DeclarationCorresponding Capability in Package
fs:server.* / fs:outer.* (or node-level fs:server.readFile etc.)fs.server.* / fs.outer.* API (server root / any path); fs.* (plugin data directory) no declaration needed
http:* (or node-level http:requestAsync etc.)fetch, request, HTTP server (createServer/listen)
service:registerNativeregisterNativeService (spawn native child process)

assets channel (assetsExtract / assetsExtractDir extraction) has no permission interception: Extraction target is强制限定 to plugin data directory plugins/<pluginName>/ (returns error if out of bounds), no declaration needed.

Rules:

  • Node-level (fs:server.readFile), group wildcard (fs:server.*), and channel wildcard (fs:*) all work; undeclared call returns Permission denied: <node> (async API is Promise reject)
  • Other nodes (service:request, assets:read, fs.* etc.) are allowed by default
  • Permissions are read and fixed when plugin loads; after consumer modifies permissions, rebuild required and full plugin reload (/yeow reload or restart server) needed to take effect — if in development mode, hot reload updates permissions simultaneously

Using getAssetsPath to Access Resources

Package internally uses getAssetsPath() to get JAR-internal paths for files in assets/:

ts
// src/index.ts
import { registerNativeService } from 'yeow-api';
import { getAssetsPath } from 'yeow-dev';   // Build-time virtual module

export const IMAGE_SERVICE = 'iyexin.image-svc.v1';

export async function registerImageService(): Promise<string> {
    const svc = await registerNativeService(IMAGE_SERVICE, {
        windows: getAssetsPath('image-svc.exe'),
    });
    await svc.ready();
    return svc.id;
}

Why import from yeow-dev? getAssetsPath must know which dependency the calling code belongs to (to inject corresponding namespace id), and yeow-dev is a build-time virtual module — builder resolves by importer ownership. yeow-dev is published as empty package (can be not installed, type declarations provided by yeow-api).

Build-time automatic handling:

  • Builder scans main project and all dependency packages' assets/, assigns unique namespace id (8-digit hex) to each dependency, copies content as-is to JAR assets/<id>/
  • getAssetsPath('image-svc.exe') after build returns "assets/<id>/image-svc.exe" (real path)
  • No additional configuration needed — build plugin automatically scans all dependency packages (see "Dependency Identification" below)

Single File & Self-Contained

Native services support single file only (string / { file }):

ts
const svc = await registerNativeService('my-svc', {
    windows: getAssetsPath('native/win/my-svc.exe'),
});

The runtime extracts only that file to a temp directory (other files in the same directory are not extracted), so the native binary must be self-contained (statically linked, or dependencies packed into a single executable). Directory mode ({ dir, entry }) has been removed.


Build-time Automatic Handling

Main project build.js uses esbuild asset plugin (yeow-assets.mjs), transparent to dependency packages:

1. yeow-api Multiple Copy Coexistence (No Dedup)

Builder does not force import 'yeow-api' to unify to main project instance — installation decided by package manager according to semantic versioning rules: When peer ranges don't overlap (e.g., main project ^0.4.0 and dependency package ^0.3.0), npm installs independent yeow-api copy for dependency package, two copies coexist in bundle.

Why safe: yeow-api is pure encapsulation of underlying protocol (no breaking changes after protocol 1.0.0), multiple copies just repeat-encapsulate same protocol; lifecycle hooks (onInit / onLoad / onUnload) register in shared global registry (__yeowInitCbs etc. — read existing, never overwrite), event handlers and handle GC queue also shared — multiple copies' callbacks enter same registry, runtime INIT/LOAD dispatch executes all, won't mutually overwrite and lose (previously due to overwriting __yeowInitCbs manifested as onLoad not executing).

2. Asset Plugin

Scans main project + all dependency packages' assets/, deploys into JAR by namespace.

Dependency Identification (node_modules scanning)

Builder scans node_modules top-level directory (including @scope/name two-level), identifies dependencies with <name>-<version> as key:

  • Identification condition: Package has an assets/ directory and satisfies one of — peerDependencies contains the yeow-api key, or it ships a yeow.config.json with permissions / native declarations (native-only / resource packages are identified even without a yeow-api dependency; their native participates in the merge and their assets are deployed)
  • Main project: Has assets/ participates (always assigned id)
  • Same-name conflict: Each dependency has independent namespace, same-name files don't overwrite each other
  • Compatibility: npm / pnpm flat layout well supported; yarn hoisting differences may cause dependencies not in expected location, if issues please use npm or pnpm

Dependency Package Permission Declaration (yeow.config.json)

Dependency packages can carry their own yeow.config.json, currently only need to declare permissions:

json
{
    "permissions": ["fs:server.readFile", "http:*", "service:registerNative"]
}

Each package only needs to declare its own permissions — npm/pnpm packages are flat-distributed, node_modules top-level packages (direct dependencies and promoted transitive dependencies) all participate in calculation, but packages don't need to consider permissions needed by their dependencies: Permissions are unified by consumer's plugin during build, dependency packages just need to clearly declare their own permissions. Packages missing yeow.config.json or permissions field contribute no permissions.

Final Permissions (computedPermissions)

Developer-declared permissions remain as-is, build automatically computes final effective permissions:

  • Merge: Main project first, dependency packages appended in order, automatically deduplicated
  • Wildcard normalization: When X:* exists (e.g., fs:*), other nodes in that channel (fs:server.*, fs:server.readFile etc.) automatically removed; when X:segment.* exists (e.g., fs:server.*), that prefix node (fs:server.readFile) automatically removed — wildcard already covers, no redundant declaration needed
  • fs:* expansion: After declaring fs:*, computedPermissions automatically expands to fs:outer.*, fs:server.* (permission semantically equivalent — plugin segment nodes allowed by default, server/outer covered by respective wildcards) — gives developers and server admins clear perception of actual impact scope (any path + server root)
  • Write-back: Result written to yeow.config.json's computedPermissions field (preserving developer-declared permissions), packaged into yeow.json for runtime to read; build terminal prints synchronously

View calculation process and permission source distribution:

bash
npm run permissions
── Permissions by source ─────────────────────────
  fs:server.*                 ← my-plugin-1.0.0
  fs:server.readFile          ← yeow-test-pkg-1.0.0
  http:*                      ← yeow-test-pkg-1.0.0
  service:registerNative      ← yeow-test-pkg-1.0.0

── Computed permissions (3) ─────────────────
  fs:server.*
  http:*
  service:registerNative

Each permission shows which package declared it, facilitating troubleshooting of permission gaps and redundancy.

Native Service Trust Declaration (native)

When a dependency package carries native service binaries, it must declare the native field in its own yeow.config.json (serviceId + binary files), fixing the SHA-256 at build time:

json
{
    "native": [
        {
            "serviceId": "iyexin.image-svc.v1",
            "files": ["native/win/image-svc.exe"],
            "source": "https://github.com/iyexin/image-svc"
        }
    ]
}
  • files are this package's assets/ binary original paths (same as paths used by getAssetsPath)
  • At build time the builder walks the main project + every dependency package's native declarations, maps them to packaged paths (assets/<id>/...) under each namespace and computes SHA-256, merging the result into yeow.json's native field; the same serviceId is merged (files consolidated)
  • When registering that native service at runtime it verifies: serviceId undeclared / binary path undeclared / SHA-256 mismatch → registration refused (Promise reject). Declaration ≠ trusted — a risk log is still printed. See Permissions & Native Service Trust

Encapsulating Service Packages

Three types of Service encapsulation (inter-plugin communication / native extension) (SDK call encapsulation / JS service / native service) and combination patterns (JS facade + native engine) have been separated into independent article: Encapsulating Service Packages. Native service's trust declaration and untrusted switch see Permissions & Native Service Trust.


Complete Example

Using yeow-image package as example (corresponding to Yeow-Test/test/yeow-image):

serviceId naming convention: Public services may be loaded by multiple packages depending on it (same-name public service globally unique — duplicate registration refused, caller uses err.serviceId for degraded access, see above type 2). To avoid conflicts between different authors' packages, serviceId should specify author.serviceName.version, e.g., iyexin.image-svc.v1:

ts
// src/index.ts
import { registerNativeService } from 'yeow-api';
import { getAssetsPath } from 'yeow-dev';

export const IMAGE_SERVICE = 'iyexin.image-svc.v1';

/**
 * Supported platforms: linux / windows / macos, subdivided by architecture (see "Supported Platforms" table below)
 */
export interface RenderResult {
    base64?: string;
    err?: string;
}

export interface ImageRenderer {
    serviceId: string;
    render(width: number, height: number, pixels: Uint8Array): Promise<RenderResult>;
}

export async function initRenderer(): Promise<ImageRenderer> {
    const svc = await registerNativeService(IMAGE_SERVICE, {
        'linux-x64':   getAssetsPath('native/linux-x64/image-svc'),
        'linux-arm64': getAssetsPath('native/linux-arm64/image-svc'),
        'windows-x64': getAssetsPath('native/windows-x64/image-svc.exe'),
        'macos-x64':   getAssetsPath('native/macos-x64/image-svc'),
        'macos-arm64': getAssetsPath('native/macos-arm64/image-svc'),
    });
    await svc.ready();

    return {
        serviceId: svc.id,
        async render(width, height, pixels) {
            const base64 = pixels.toBase64(); // ES2026 native
            return (await svc.request('/imageRender', {
                body: { width, height, base64 },
            })).json() as Promise<RenderResult>;
        },
    };
}

Supported platforms: Keys support operatingSystem or operatingSystem-architecture granularity, exact match (including architecture) prioritized, falls back to operating system if not found:

assets/
├ native/
│  ├ linux-x64/image-svc
│  ├ linux-arm64/image-svc
│  ├ windows-x64/image-svc.exe
│  ├ macos-x64/image-svc
│  └ macos-arm64/image-svc
KeyDescription
windows / windows-x64Windows (x64 or any architecture)
linux / linux-x64 / linux-arm64Linux x86_64 / ARM64
macos / macos-x64 / macos-arm64macOS Intel / Apple Silicon

Recommend providing at least windows-x64 + linux-x64 + linux-arm64: Most Paper servers deploy on Linux x64 VPS, Linux ARM (Raspberry Pi/NAS/ARM cloud host), or Windows x64. When missing current platform configuration, registration returns error No binary for platform: <os> (<os>-<arch>).

Consumer in main plugin:

json
// Main plugin package.json
{
    "dependencies": {
        "yeow-api": "^0.4.0",
        "yeow-image": "^0.0.1"   // Example
    }
}
ts
// Main plugin src/index.ts
import { initRenderer } from 'yeow-image';

onLoad(async () => {
    const renderer = await initRenderer();
    const result = await renderer.render(2, 2, pixels);
});

Dependency package declares permissions: yeow-image should declare permissions: ['service:registerNative'] in its own yeow.config.json

Encapsulation recommendation: Encapsulate all details like serviceId, ready-wait etc. within package, externally only expose high-level operation functions (e.g., renderer.render()). Consumers don't need to know about Native Service's existence.


Checklist

  • [ ] files includes assets/
  • [ ] main / types points to src/index.ts
  • [ ] peerDependencies declares yeow-api version range (widest compatible range; missing declaration → assets/permissions not participating in build)
  • [ ] devDependencies declares development target version yeow-api (independent development type checking)
  • [ ] When using fs/http/registerNative, declare in own yeow.config.json
  • [ ] Resources obtained via getAssetsPath(), not hardcoded