npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iobroker-adapter-checks

v0.32.0

Published

Repository standard checks for ioBroker adapters — runs as a plain unit test on any dev system, no python, no network.

Readme

iobroker-adapter-checks

Repository standard checks for ioBroker adapters. Plain JavaScript, no python, no network, no child processes — it reads files and returns findings, so it runs as an ordinary unit test on any developer machine and in CI.

Use

// test/standards/repo-standards.test.ts
import { join } from "node:path";
import { allChecks, formatFindings } from "iobroker-adapter-checks";

const adapterDir = join(__dirname, "..", "..");

describe("repository standards", () => {
  for (const check of allChecks) {
    it(check.title, () => {
      expect(formatFindings(check.run(adapterDir))).toBe("");
    });
  }
});

vitest must be told to look outside src/:

include: ["src/**/*.test.ts", "test/standards/*.test.ts"],

The ioBroker test action runs npm run test:unit, so the checks then run in the standard matrix without any workflow change.

Checks

| id | what it catches | why it matters | |----|-----------------|----------------| | switch-default | a switch over a message command without default: | an unknown command never answers the caller — it hangs until the ioBroker timeout | | changelog-count | more than 7 versioned entries in the README changelog | repochecker E6006 | | device-id-scheme | an object inventory with device objects but no deviceIds in fleet.json; with "unit", a device object without native.idScheme: 3 or with an id that is not lower-case letters and digits joined by hyphens | ids from an older rule move again later, and the user's scripts, aliases and recordings point at the old one; an adapter without the declaration would stand outside the rule | | node-matrix | a CI matrix entry below engines.node — flow list or block list | the install fails with EBADENGINE | | secret-fields | encryptedNative / protectedNative nested under common | js-controller ignores them there — credentials end up unencrypted | | english-only | German prose in README or in common.news[*].en | repochecker guards the README (E6015) but never looks at the release notes | | changelog-style | release notes that name build tools, test runners or internal identifiers | that text is what users read in the admin update dialog | | admin-i18n | settings-page texts missing from a shipped language | the admin shows a half-translated dialog | | readme-requirements | a requirement statement before the changelog heading ([ioBroker ]<dependency> >= <version> in any form — bold, list item, running text, with text after it) that differs from the manifest, or a Node version below engines.node | the user follows the README and the install refuses the adapter, or is sent to upgrade for nothing | | stop-instance | common.supportedMessages.stopInstance set in the manifest, or a supportedMessages object without any value other than false | the host kills the process instead of asking it to stop — onUnload never runs; an all-false list turns the messagebox off and every sendTo goes nowhere | | sentry-disclosure | an adapter shipping the Sentry plugin without the repository checker's Sentry notice near the top (before the fifth ## heading) | crash reports leave the user's machine and nothing near the top of the page mentions it (W6023/W6024) | | error-text-selfstate | a reason text that restates the adapter's own run state ("adapter is stopped") | it occupies the slot meant for the real cause, and the user already sees that the instance is off | | local-artifacts | a local artifact in the repository root — node_modules, .dev-server, coverage, .env — that no .gitignore rule covers | the next broad git add publishes a working directory, dev-server profiles carry the developer's hostname | | issue-forms | no issue form, a config.yml that still allows blank issues, or a legacy Markdown template beside the forms | reports arrive without version, log or steps, and the first reply is a question back to the reporter | | first-reply-workflow | no workflow reacting to issues: opened (repositories of a centrally managed organisation are left alone) | every report waits for a human to ask the fixed questions; a report against an outdated version looks like a regression | | release-deploy-gate | a tag-triggered deploy whose test jobs skip their steps on tags while nothing waits for the branch run of the same commit (or the wait lacks actions: read); or a job the deploy depends on whose if: reads the ref or the event in a form the check cannot judge | a tagged release can publish a tree nobody tested — the dependencies are green because they were skipped | | common-docs | common.docs missing a language, not starting with docs/<lang>/README.md, linking a file that does not exist, leaving a page in docs/<lang>/ unlinked, carrying different chapters in en and de, or a README that does not link docs/<lang>/README.md of every documented language | the documentation portal shows exactly the linked pages — an unlinked page is invisible, a missing language falls back to the README, and a chapter present in one language only leaves the other language's readers without it; on GitHub and npm the README is the only page a reader lands on (ioBroker.repositories README: "make the link in your readme file to these files") | | instance-objects-refresh | a manifest object (instanceObjects) that no extendObject("<id>", …) below src/ refreshes, or one refreshed only in a method nothing calls | js-controller applies the manifest on every start but preserves common.name — a renamed object keeps its old name on every existing installation, and neither lint nor tsc see a call site that was dropped | | messagebox-repair | code that repairs common.supportedMessages by writing an object instead of deleting the key (: null, = null or delete …), or that triggers the repair on stopInstance instead of on the key existing; for an adapter whose manifest declares a real entry (deviceManager) the opposite — deleting the key | supportedMessages is a positive list: with an object there and no value other than false, the host never subscribes to messages — no sendTo reaches the adapter, without a log line | | listen-port-declaration | an adapter that opens a socket without declaring its ports in fleet.json (listenPorts), a declared primary port that is not a number in native.port, a listen address stored under any key but native.bind (bindAddress, BIND), a port field that is not of type port (or, for a protocol-fixed port, not disabled with min = max), a bind field that is not an ip field offering 0.0.0.0, a port the code fixes (a number or a named constant) that listenPorts does not declare — and native.bind on an adapter that declares no listener | the admin's port-conflict check ("Port is already used by X") only sees instances carrying both native.port and native.bind — measured on the official repository, 145 of 801 adapters declare the port and only 38 the bind; a listener stored otherwise is invisible, and a client adapter carrying bind would be reported as holding the port of its peer | | fetch-stub-response | a test or fixture that replaces fetch (vi.stubGlobal("fetch", …), globalThis.fetch = …, vi.spyOn(globalThis, "fetch")) and answers with a hand-built object carrying json/text methods instead of new Response(body, { status, headers }) | the imitation has no body, headers or clone() — code that reads the stream (response.body.getReader()) sees undefined, and the test reports a path as covered that it never ran (ai-usage 2026-09-16: every provider returned an empty string while the fixture looked healthy) | | object-rewrite | an object rewritten by delObject + setObjectNotExists/extendObject of the same id on the same path through a function, or written whole with setObject/setObjectAsync on the adapter | a deleted state object loses its value and its enum memberships (the user's room and function assignments — _delForeignObject calls delForeignState and removeIdFromAllEnums), and nothing the recreate writes brings them back; setObject is the repository checker's S5054 and setObjectAsync the same call unseen by its list. Dropping a key from an existing object is one write: read it, remove the key from the copy, setForeignObject(fullId, copy) | | deprecated-adapter-methods | a call on the adapter (this in a class extending …Adapter, adapter, ….adapter) to a method the INSTALLED @iobroker/types marks @deprecated — read from its declaration files, so the list follows the version the adapter resolves (7.2.2: setStateAsync, extendObjectAsync, setObjectAsync, setForeignObjectAsync, createState, deleteState and their relatives) | a deprecated method is the next removal; the adapter's own type check already shows it struck through. Silent without node_modules/@iobroker/types | caught-value-text | a caught value — the variable of a catch, the parameter of a .catch(…)/.then(…, …) rejection handler, a copy of it (const err = error as Error), the reason of a rejected Promise.allSettled result, or the parameter of any helper such a value is passed to — turned into text with String(err), ${err}, (err as Error).message or a bare JSON.stringify(err), outside a branch that rules an object out (typeof err !== "object", err instanceof Error, …) — and its .message shown as text (a template, +, a call or new argument, a return, an object property) in any branch, unless the same function reads its .cause; below src/ and, for an Admin 8 component, src-admin/src/ | JavaScript lets code throw anything: String() and a template render a thrown plain object (a rejected { code: "ECONNRESET" }, an HTTP client's error object) as [object Object], a template on a thrown symbol throws again inside the catch block, the cast reads undefined off a thrown string, and JSON.stringify throws on a circular structure; an Error's .message alone drops its reason — Node's fetch rejects every network failure as fetch failed with the reason only in cause. One helper handles every thrown value: Error → message (its code when empty) with one level of cause, string → itself, a function or class → Object.prototype.toString.call (its type tag, never its source), other primitives → String(), objects → JSON.stringify with Object.prototype.toString.call as the fallback — all inside one try/catch whose catch returns the type tag | | error-text-helper | a second function carrying the object branch of the error-text helper (JSON.stringify(…) together with Object.prototype.toString.call(…)) below src/ or src-admin/src/ | a copy drifts from the helper it was taken from; the reason given for Admin copies ("the component cannot import src/") is a declaration file the build writes next to the imported source — the Admin component imports the adapter's helper from src/ (with dts: false in the module-federation plugin the build writes no declaration next to the source) | | error-text-reason | the repository's error-text helper (the first function below src/ carrying the object branch) whose body never reads a property cause, never reads a property code, never tests typeof … === "function", or reads message, name, code or cause outside the protected block of a try — structural proxies: they prove the helper looks at each, its own tests prove the words | ES2022 gave Error a cause, and Node's fetch rejects with new TypeError("fetch failed", { cause }) — an unreachable host, a refused port and a dropped socket all read fetch failed when the helper returns the message alone; http.get and net.connect to localhost reject with an AggregateError whose message is empty and whose code is ECONNREFUSED, so the message alone is nothing (Node 22.22.2). Render the message (its code when empty) and one level of cause: fetch failed (getaddrinfo ENOTFOUND host). The helper runs inside a catch: a getter that throws, or a message that is not a string, must not make it throw a second time — the whole body sits in a try whose catch returns the type tag; a thrown function or class is a typeof "function", and String() would print its source | | object-delete-drops-state | delState/delForeignState (or their Async twins) right before or after delObject/delForeignObject of the same id on the adapter, on the same path through a function | js-controller deletes the value of a state object together with the object (_delForeignObject → delForeignState, plus the enum memberships — since 1.x): the extra delete is a second round trip per object and a comment that misleads the next cleanup routine. A delState on its own (an orphan value) is not judged | | fire-and-forget-rejection | a promise dropped with void whose rejection has no receiver: the callee (a method of the class, a function of the file or behind a relative import, an immediately invoked arrow) lets an await, a for await, a throw, a returned promise or an own helper that throws escape its try/catch — before the try, after the catch, inside a try without catch clause, or as a rethrow in the catch clause — or a .then(…) chain without .catch(…) whose root or callback can reject; an awaited own function whose body is one try/catch is no risk, and a callee the check cannot resolve (setState, a library) is not judged | an unhandled rejection ends the instance: js-controller logs it and stops the process (_exceptionHandler, exit code UNCAUGHT_EXCEPTION), and whatever the call was writing is lost. Below src/; synchronous calls the check cannot resolve (a logger, Object.keys) are not read | | read-stub-copy | a test stub of an adapter read method (getObject, getForeignObject, getForeignObjects, getState, getForeignState, getStates, getEnums, getObjectView, … and their Async twins) that answers with the object it keeps — a lookup (store.get(id), objects[id]), a member of the harness (this.instanceObject) or a variable declared outside the stub — also as a member of an object the stub builds for an object read ({ common: this.instanceCommon }, { native: { devices: stored } }) — followed through Promise.resolve, await, ??, ?: and the stub's own variables; a callback-form name (getStates) counts only next to another adapter member or on a receiver called adapter/this, because other libraries a test fakes use the same names | the controller hands every read a fresh value — with a shared reference a change the code makes on what it read is in the store before any write, so no test can tell a missing write from a done one (hassemu 2026-09-17: the repair path's write was invisible to every test); answer with structuredClone(obj), null stays null | | override-below-parent | an overrides entry in package.json that installs a dependency below the lowest version its dependant declares (judged from package-lock.json, the tree npm ci installs; an override that lifts a dependency above the declared range is allowed) | a security override is written against the dependant of its day — once the dependant moves on, the same entry holds the dependency below what it now declares (measured 2026-09-24: mocha 12 declares diff ^9.0.0, the mocha-11 era entry installed diff 8.0.4), a version the dependant was never released with; remove the entry or raise it | | system-config-fields | a read of dateFormat, isFloatComma, language, longitude or latitude on the adapter (this in a class extending …Adapter, adapter, ….adapter) while no object literal in the sources sets useFormatDate: true; a field the adapter declares or assigns itself is its own, and systemConfig: true does not count — it fills this.systemConfig from iobroker.json | js-controller copies the five fields from system.config only under useFormatDate: true (@iobroker/js-controller-adapter 7.2.2) — without it the field stays undefined and the adapter falls back in silence: labels in English in every system language, no Accept-Language, no location | | migration-result-used | a call of migrateNativeKeys (the fleet helper) whose result is dropped — an expression statement, awaited or not, or under void; a wrapper that returns the result passes the duty to its callers, followed by name | the helper returns true when it wrote the instance object, and js-controller restarts the instance on every change of it — a start that goes on binds ports, opens connections and writes states in a process that is about to be stopped | | instance-enabled | common.enabled missing or not false | the object schema and js-controller's io-package schema say it should be false, so a new instance starts disabled; an update keeps the setting of an existing instance (extendCommon preserves enabled) | | i18n-before-messages | in an adapter that uses I18n: I18n.init that is not the first statement of its function (the first statement of a leading try counts), a device manager (a class extending DeviceManagement) constructed anywhere but after I18n.init in the same function, a message handler registered in a constructor (this.on("message", …) or a message option to super) | js-controller delivers messages before onReady has finished, dm-utils listens from its constructor on, and I18n throws before init — a device-manager message during a restart answers nothing; the order is the one ioBroker.shelly uses | | process-handlers | a process-wide uncaughtException or unhandledRejection handler (process.on, once, addListener, prependListener) below src/ | js-controller's adapter class already owns both handlers (adapter.ts) — a second one runs beside it, and in compact mode it catches the errors of every other adapter in the same process | | no-python | a .py file anywhere in the repository except node_modules, .git, build, coverage and .dev-server | the build and release environment of an adapter is npm only; a Python helper is a second toolchain nobody else can run | | log-language | German in the literal text of a log call or of new …Error(…) — umlauts or high-confidence German words; an interpolated ${…} (a device name) does not count | ioBroker logs are English: users and maintainers search and report them in one language (ioBroker review, ioBroker.repositories #5667) | | handler-guard | a handler registered with this.on("<event>", this.x) where x is a method and .bind(this) is missing, or an async handler (method, arrow-function property or inline arrow) whose body is not one top-level try — statements around it must catch their own rejection | nobody awaits a lifecycle handler: a rejection becomes an unhandled rejection, and js-controller ends and restarts the instance |

Each check reads only the adapter it is pointed at and never writes. object-rewrite, deprecated-adapter-methods, caught-value-text, error-text-helper, error-text-reason, object-delete-drops-state, fire-and-forget-rejection, read-stub-copy, system-config-fields, migration-result-used and i18n-before-messages parse the sources with the adapter's own typescript (a required peer dependency — every TypeScript adapter has it, 5.x or 6.x; TypeScript 7 ships no JavaScript compiler API these checks can call); when no compiler can be loaded they report that as a finding instead of staying silent.

Readable values (command)

iobroker-adapter-checks values judges what a user actually sees in the object tree: the value of every state and the labels of its common.states list. It reads the dumps an inventory run writes and is not part of allChecks — a plain npm test never runs it.

npx iobroker-adapter-checks values \
  --objects test/objects.inventory.json \
  --states test/states.inventory.json \
  --objects-other-language test/objects.inventory.de.json \
  --declarations test/readable-values.json

| rule | reports | |------|---------| | state-list | a value that is not in common.states (numbers with min/max: outside the range and not a listed special value) — "Only these values are allowed", ioBroker objectsschema.md | | label-repeats-key | a label that only repeats its value ({off: "off"}, the deprecated array form) | | label-language | a label that is not a plain string, or stays the same in a run with a second system language (numbers and unit symbols such as °C, kWh, % do not count as words) | | enum-without-list | a read-only text state whose value is an identifier (cupboarddryplus) and that has no list of labels | | encoded-value | raw JSON as text, base64 that decodes to binary data, long hex blocks | | onoff-as-text | a text state whose list is exactly one on/off pair (on/off, true/false, yes/no, enabled/disabled, active/inactive, open/closed) — on/off is a boolean in the role catalog | | info-role | a leaf named firmware, hardware, model, serial, mac, ip or port without the catalog's info.* role | | unit-on-non-number | a common.unit on a boolean or text state | | min-above-max | common.min above common.max | | step-not-positive | common.step of 0 or below | | default-not-allowed | common.def outside min/max, or not in common.states where the list is the whole range | | list-keys-type | a number state whose common.states keys are not numbers | | id-language | an object id that only one of the two language runs created — ids are fixed English and never depend on the system language |

What an adapter cannot change is declared in test/readable-values.json: { "<pattern>": { "<rule>": "<reason of at least 15 characters>" } }, the pattern without namespace, * standing for exactly one id segment. A label text that is the same word in every language (an Italian coffee name, a brand) is listed once under "$sameInEveryLanguage": { "<label text>": "<reason>" } instead — a pattern-wide exception would also hide the labels of the same list that do need a translation. A declaration that matches nothing, covers datapoints the rule does not flag, lists a label that no longer stays the same, or gives no real reason is itself reported. Without --objects-other-language the command stops with exit 2 unless --single-language says the language rule is not judged. On one installation's live tree (switched-off groups, a single device) --partial-tree reports the coverage of the declarations as not judged — only the full inventory can refute a declaration. Exit 0 = clean, 1 = findings, 2 = could not judge.

Options

runChecks(dir, options) and every check.run(dir, options) accept:

| option | effect | |--------|--------| | skip | check ids to leave out | | maxChangelogLineLength | switches on the release-note length rule (off by default — ioBroker has no such limit; the repository checker counts entries, not characters) |

A repository that iterates allChecks itself leaves a check out by filtering the list — for example allChecks.filter((c) => c.id !== "first-reply-workflow") — with the reason written next to it.

Changelog

See CHANGELOG.md.

Origin

These started as python scripts in a private release toolchain. mcm1957 asked for JavaScript so they can run on every dev system (ioBroker.yamaha#609); this package is that migration. Every check was verified against the python original on eleven adapters: same finding, same silence.