readme: quick start, generated capability table, recipes, bridge contract, migration
The readme is rewritten for 2.0: what the library needs, install, the lamp as quick start, concepts, the table of interfaces, recipes, the bridge contract, errors, testing without Alexa, migration from 1.x and limits. scripts/capability-table.js (npm run docs) writes the table from the registry and the text of three example files into their code blocks; "Through Alexa" names only the runs of 2026-09-28, made with 1.5.2. test/readme.test.js fails on a stale table or file, runs examples/testing/lamp.test.js (MemoryPublisher, no broker) and compiles the other code blocks. package.json: description, keywords, CHANGELOG.md in files. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a59de926a2
commit
785944b2da
6 changed files with 848 additions and 174 deletions
119
scripts/capability-table.js
Normal file
119
scripts/capability-table.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"use strict";
|
||||
// Writes what readme.md takes from the code: the table of interfaces, from the registry, between the two marker
|
||||
// lines, and the text of a file into the code block that follows a line <!-- file: examples/lamp.js -->.
|
||||
//
|
||||
// npm run build && npm run docs write them
|
||||
// node scripts/capability-table.js --check exit 1 when the readme has another text
|
||||
//
|
||||
// It reads the built registry (dist/cjs), so the build comes first. test/readme.test.js fails on a stale readme.
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
const README = path.join(ROOT, "readme.md");
|
||||
const BEGIN = "<!-- capabilities: written by scripts/capability-table.js, npm run docs -->";
|
||||
const END = "<!-- /capabilities -->";
|
||||
|
||||
// What was driven from a real Alexa account through the public Alex2MQTT service, and when. The runs used
|
||||
// alex2node 1.5.2; a row is added here when a run is recorded, not when a descriptor is written.
|
||||
const DRIVEN_ON = "2026-09-28";
|
||||
const DRIVEN_WITH = "alex2node 1.5.2";
|
||||
const DRIVEN = {
|
||||
"Alexa": "ReportState",
|
||||
"Alexa.BrightnessController": "directives",
|
||||
"Alexa.ColorController": "directives",
|
||||
"Alexa.ColorTemperatureController": "directives",
|
||||
"Alexa.ContactSensor": "change report",
|
||||
"Alexa.LockController": "directives, change report",
|
||||
"Alexa.ModeController": "directives",
|
||||
"Alexa.MotionSensor": "change report",
|
||||
"Alexa.PercentageController": "directives",
|
||||
"Alexa.PowerController": "directives, change report",
|
||||
"Alexa.PowerLevelController": "directives",
|
||||
"Alexa.RangeController": "directives",
|
||||
"Alexa.SceneController": "directives",
|
||||
"Alexa.TemperatureSensor": "change report",
|
||||
"Alexa.ThermostatController": "directives",
|
||||
"Alexa.ToggleController": "directives",
|
||||
};
|
||||
|
||||
const code = (names) => (names.length > 0 ? names.map((name) => `\`${name}\``).join(", ") : "-");
|
||||
|
||||
function row(descriptor) {
|
||||
const { namespace, version, doc, tier, instanced, properties, directives, events = {} } = descriptor;
|
||||
return [
|
||||
`[${namespace}](${doc})${instanced ? " (instances)" : ""}`,
|
||||
version,
|
||||
code(Object.keys(properties)),
|
||||
code(Object.keys(directives)),
|
||||
code(Object.keys(events)),
|
||||
String(tier),
|
||||
DRIVEN[namespace] ?? "-",
|
||||
];
|
||||
}
|
||||
|
||||
/** The text between the markers, from the registry of the build. */
|
||||
function table(registry = require(path.join(ROOT, "dist", "cjs", "index.js")).registry) {
|
||||
const all = registry.list();
|
||||
const unknown = Object.keys(DRIVEN).filter((namespace) => !registry.has(namespace));
|
||||
if (unknown.length > 0) throw new Error(`DRIVEN names ${unknown.join(", ")}, which the registry does not have`);
|
||||
|
||||
const described = all.filter((descriptor) => descriptor.tier !== 3);
|
||||
const named = all.filter((descriptor) => descriptor.tier === 3);
|
||||
const header = ["Interface", "Version", "Properties", "Directives", "Events", "Tier", "Through Alexa"];
|
||||
const lines = [header, header.map(() => "---"), ...described.map(row)].map((cells) => `| ${cells.join(" | ")} |`);
|
||||
return [
|
||||
...lines,
|
||||
"",
|
||||
`${described.length} interfaces are described (tier 1 and 2), ${named.length} are named (tier 3).`,
|
||||
`"Through Alexa" is what a real Alexa account drove through the public Alex2MQTT service on ${DRIVEN_ON}, with`,
|
||||
`${DRIVEN_WITH}; "-" is an interface that no such run has covered.`,
|
||||
"",
|
||||
`Tier 3: ${named.map(({ namespace, version, doc }) => `[${namespace}](${doc}) ${version}`).join(", ")}.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// A line that names a file, and the code block after it
|
||||
const FILE = /<!-- file: (\S+) -->\n```(\w+)\n[\s\S]*?\n```/g;
|
||||
|
||||
/** The readme with the text the named files have now in their code blocks. */
|
||||
function files(readme) {
|
||||
return readme.replace(FILE, (block, file, language) => {
|
||||
const text = fs.readFileSync(path.join(ROOT, file), "utf8").trimEnd();
|
||||
if (text.includes("```")) throw new Error(`${file} has a code fence, which would end its block in readme.md`);
|
||||
return `<!-- file: ${file} -->\n\`\`\`${language}\n${text}\n\`\`\``;
|
||||
});
|
||||
}
|
||||
|
||||
/** The files the readme shows, as the marker lines name them. */
|
||||
function shown(readme) {
|
||||
return [...readme.matchAll(FILE)].map(([, file]) => file);
|
||||
}
|
||||
|
||||
/** The readme with the table of now and the files of now. Throws when the markers are not there, each once and in this order. */
|
||||
function update(readme, text = table()) {
|
||||
const begin = readme.indexOf(BEGIN);
|
||||
const end = readme.indexOf(END);
|
||||
if (begin < 0 || end < begin || readme.indexOf(BEGIN, begin + 1) >= 0 || readme.indexOf(END, end + 1) >= 0) {
|
||||
throw new Error(`readme.md needs the lines "${BEGIN}" and "${END}", once each and in this order`);
|
||||
}
|
||||
return files(`${readme.slice(0, begin)}${BEGIN}\n${text}\n${readme.slice(end)}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const before = fs.readFileSync(README, "utf8");
|
||||
const after = update(before);
|
||||
if (process.argv.includes("--check")) {
|
||||
if (after !== before) {
|
||||
console.error("readme.md has another table of interfaces or another text of a file than the code gives: npm run build && npm run docs");
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (after !== before) {
|
||||
fs.writeFileSync(README, after);
|
||||
console.log("readme.md was written");
|
||||
} else {
|
||||
console.log("readme.md is up to date");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { table, update, files, shown, BEGIN, END, DRIVEN, README };
|
||||
Loading…
Add table
Add a link
Reference in a new issue