"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 . // // 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 = ""; const END = ""; // 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 = /\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 `\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 };