@12-apps/wiring

中文文档
1.17.0发布于 2026/10/02MITES Module零依赖构建来源已验证近 30 天下载 3.4万

The package-to-host wiring contract: a producer half for packages (capability manifests — HTTP routes, job blueprints, MCP tools with annotations, permissions, notification blueprints, mailers, Prisma partials, web surfaces) and a consumer half for hosts

原始使用文档
文档目录8 节
  1. The problem
  2. The model
  3. Capabilities
  4. React Native hosts
  5. Ports ( @12-apps/wiring/ports )
  6. What stays host-owned
  7. Example
  8. Status

@12-apps/wiring

The package-to-host wiring contract. A producer half for packages — declare what you provide and what you require, per capability — and a consumer half for hosts — adopt the declarations, bind each one against shared ports, and get one ordered, conflict-checked, reported aggregate back.

Zero dependencies. Every capability shape is a structural twin of a seam a package in this repo already ships, so adopting the contract is declaring what already exists, not rewriting it.

The problem

Every @12-apps/* package already follows the same implicit plugin shape — createApi*(config) → { routes }, createWeb*(config) → { page, … }, a ./hono adapter, a Prisma partial, sometimes ./mcp or job blueprints. But the shape lives in TypeScript conventions and per-package ADOPTING.md files, so:

  • every host rediscovers each package's contract by reading it;
  • the origin host wires ~24 packages through hand-written surface files, route files, nav files, icon files, badge files and policy tables;
  • shapes that must stay identical across packages (JobBlueprint vs PaymentsJobBlueprint) are kept identical by convention, with no compile check anywhere;
  • a version bump that adds a capability arrives silently: report-builder 5.x shipped three working-copy endpoints its own client calls, and the origin host never mounted them — the editor's autosave 404s and nothing is red;
  • packages that should notify or mail cannot say so: there is no notify port at all, and the only email port lives inside another package's server entry.

The model

        PRODUCER (the package)                CONSUMER (the host)
  ┌──────────────────────────────┐      ┌────────────────────────────────┐
  │ <pkg>/manifest               │      │ createWiringHost({name, kind,  │
  │   defineManifest({           │      │                   ports})      │
  │     name, contract: 1,       │      │                                │
  │     permissions, mcp,        │──────▶ .adoptServer({manifest,        │
  │     notifications, db, e2e,  │      │    server, bindings})          │
  │     server: ['http','jobs'], │      │ .adoptWeb({manifest, web,      │
  │     web: ['surface','areas'] │      │    bindings})                  │
  │   })                         │      │                                │
  │ <pkg>/manifest/server        │      │ .assemble() ──▶ routes, jobs,  │
  │   defineServerManifest(...)  │      │    mailers, surfaces,          │
  │ <pkg>/manifest/web           │      │    permissions, notifications, │
  │   defineWebManifest(...)     │      │    mcpEndpoints, db, areas,    │
  └──────────────────────────────┘      │    report                      │
                                        └────────────────────────────────┘

Separate manifests per runtime because bundles are physics: the shared manifest is data every runtime can hold; the server manifest carries the createApi* factory and job blueprints; the web manifest carries the createWeb* factory; a package with React Native screens adds a native manifest (see React Native hosts). The shared manifest inventories the runtime manifests, and that inventory is the integrity mechanism: the producer refuses a runtime manifest that drifts from it, and a host that adopts a manifest without answering an inventoried capability gets a red assemble() naming the package and the capability.

Every declared capability must be bound or declined with a written reason. Declining is legitimate — a harness with no worker declines jobs — but silence is not: assemble() throws while anything is unanswered. That is what turns "the bump shipped a capability the host never wired" from a silent 404 into a build failure.

Capabilities

kindproducer declaresconsumer binds withaggregate
httpcreate(config) → { routes } — the existing createApi* factory{ mountPath, config }routes, specificity-ordered, conflict-checked
jobsnamespace + dep-free blueprints (twin of @12-apps/jobs' JobBlueprint){ deps }jobs — JobDefinition twins; jobs.map(defineJob) and done
emailcreateMailer(port) → mailer (the createAuthMailer pattern){ port? } (defaults to the host port)mailers[pkg]
mcpWireMcpTool[] — McpEndpoint plus annotationscollected; host-built vocabulary tools join via mcpEndpointsmcpEndpoints, uniqueness-checked
permissionsan rbac-shaped contribution (source, ids, permissions, labels)collectedpermissions — feed composePermissions
notificationsblueprints: type, suggested category, generatecollectednotifications — feed the notifications mount
dbthe Prisma partial + migrations pathscollecteddb — feed the sync tooling
surfacecreate(config) → surface — the existing createWeb* factory{ config }surfaces[pkg], built once (the memoisation rule, held in one place)
surface (native)the same create(config) → surface, returning React Native component types, in <pkg>/manifest/native{ config } on a native hostsurfaces[pkg], built once
areasroute/nav/gate suggestions per host areacollectedareas — project nav from data
e2ethe journeys' entry subpathcollectedin the report

React Native hosts

A React Native app is a third host kind. A package that ships screens for one adds a fourth manifest, <pkg>/manifest/native, declared with defineNativeManifest and inventoried as native: ["surface"] in the shared manifest. Its surface has the web surface's shape — one config object in, component types out — but returns React Native components, because a native bundle can import neither react-dom nor a web component library, and a web bundle must never import React Native.

// package: <pkg>/manifest/native — a plain value; wiring stays a type-only edge
import type { AnyNativeManifest } from "@12-apps/wiring";
export const threadsNativeManifest = {
  name: "@12-apps/threads",
  surface: { create: createNativeThreads },
} as const satisfies AnyNativeManifest;

// package: its own test suite runs the producer's assertions
expect(defineNativeManifest(threadsManifest, threadsNativeManifest)).toBe(threadsNativeManifest);

// host: the React Native app
const host = createWiringHost({ name: "field-app", kind: "native", ports: { loggerFor } });
const { surface } = host.adoptNative({
  manifest: threadsManifest,
  native: threadsNativeManifest,
  bindings: { surface: { config: { fetch, copy } } },
});
host.assemble();

surface is ONE capability with two possible runtimes, so the vocabulary every package answers for does not grow: the conformance gate checks only that a native: inventory has its ./manifest/native subpath, and that the subpath does not outlive the inventory (assertExportsMirror checks the same in the package's own suite). A native host has no areas (its navigation is its own; a native manifest carrying areas is refused), reports the server and web halves out-of-scope with the runtime named on each entry, and answers env vars declared with scope: "native". Like a web host, it still collects the shared data capabilities (permissions, notifications, MCP, db) into its aggregate; it simply has nothing to feed them to.

Ports (@12-apps/wiring/ports)

What a host provides once, for every package to require: EmailPort (the EmailDriver shape, verbatim), NotifyPort (new — emit a typed notification event; never throws), LoggerPort, JobsEnqueuePort (never throws), ClockPort. Memory reference implementations ship for tests.

What stays host-owned

The contract deliberately moves no decision that is host vocabulary today: every user-facing sentence (pt-BR copy arrives as config, exactly as report-builder 4.0 inverted it), guards and actor resolution, plan gates, nav placement/labels/icons, MCP operation ids and summaries for vocabulary-driven tools, the RBAC catalog assembly, and the database itself. The contract makes each of those a named, typed, refusable argument instead of a convention.

Example

See src/__tests__/fixture-package.ts for a complete producer, and src/__tests__/consumer.test.ts for both host kinds. The short version:

// host (API server)
import { createWiringHost } from "@12-apps/wiring/consumer";
import { defineJob } from "@12-apps/jobs";

const host = createWiringHost({ name: "web", kind: "server", ports: { email } });
host.adoptServer({
  manifest: notesManifest,
  server: notesServerManifest,
  bindings: {
    http: { mountPath: "/api/admin/:tenantSlug", config: { store } },
    jobs: { deps: { store, ran } },
    email: {},
  },
});
const wired = host.assemble();          // throws while anything is unanswered
wired.jobs.map((job) => defineJob(job)); // every adopted package's jobs, one line
console.log(renderWiringReport(wired.report));

Hosts that must keep one route file per endpoint (coverage gates read guards out of route files) still adopt: they mount by hand and pin unclaimedRoutes(wired.routes, claimedKeys) to [] in a unit test, which is the test that would have caught the working-copy 404.

Status

Released: the package is in release-packages.txt (first — it has no dependencies) and @12-apps/report-builder is the first producer, adopted by the consumer harness. The RFC and the per-package adaptation report live in docs/wiring/.

版本与发布记录

@12-apps/wiring 共发布 20 个版本,首次发布于 2026/08/20;过去 12 个月发布了 20 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.17.0latest2026/10/021 个月前
1.16.02026/08/262 个月前
1.15.02026/08/262 个月前
1.14.02026/08/252 个月前
1.13.02026/08/252 个月前
1.12.12026/08/252 个月前

关于 @12-apps/wiring 的常见问题

@12-apps/wiring 是什么?

The package-to-host wiring contract: a producer half for packages (capability manifests — HTTP routes, job blueprints, MCP tools with annotations, permissions, notification blueprints, mailers, Prisma partials, web surfaces) and a consumer half for hosts

如何安装 @12-apps/wiring?

在项目目录运行 npm install @12-apps/wiring,或使用 pnpm add / yarn add / bun add。

@12-apps/wiring 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/12-apps__wiring 是否存在。

@12-apps/wiring 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@12-apps/wiring 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@12-apps/wiring 可以商用吗?

@12-apps/wiring 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@12-apps/wiring 的下载量有多少?

根据 npm 官方下载统计,@12-apps/wiring 在 2026/10/05 – 2026/11/03 这 30 天内被下载 34,031 次,日均约 1,134 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@12-apps/wiring 还在维护吗?

最新版本 1.17.0 发布于 2026/10/02(1 个月前);至今共发布 20 个版本,其中过去 12 个月发布了 20 个;首次发布于 2026/08/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。