homebridge-plugin-utils 1.35.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +130 -2
  2. package/build/eslint-plugin/README.md +164 -0
  3. package/build/eslint-plugin/config.mjs +308 -0
  4. package/build/eslint-plugin/index.mjs +7 -0
  5. package/build/eslint-plugin/plugin.mjs +46 -0
  6. package/build/eslint-plugin/rules/blank-line-after-open-brace.mjs +137 -0
  7. package/build/eslint-plugin/rules/blank-line-after-open-brace.test.mjs +112 -0
  8. package/build/eslint-plugin/rules/comment-style.mjs +190 -0
  9. package/build/eslint-plugin/rules/comment-style.test.mjs +190 -0
  10. package/build/eslint-plugin/rules/enforce-node-protocol.mjs +114 -0
  11. package/build/eslint-plugin/rules/enforce-node-protocol.test.mjs +116 -0
  12. package/build/eslint-plugin/rules/paren-comparisons-in-logical.mjs +94 -0
  13. package/build/eslint-plugin/rules/paren-comparisons-in-logical.test.mjs +107 -0
  14. package/build/eslint-plugin/rules/split-type-imports.mjs +354 -0
  15. package/build/eslint-plugin/rules/split-type-imports.test.mjs +268 -0
  16. package/build/eslint-plugin/test-setup.mjs +21 -0
  17. package/build/fs-ops.mjs +184 -0
  18. package/build/tsconfig.json +18 -3
  19. package/dist/backpressure.d.ts +94 -45
  20. package/dist/backpressure.js +229 -93
  21. package/dist/backpressure.js.map +1 -1
  22. package/dist/cli/index.d.ts +166 -0
  23. package/dist/cli/index.js +551 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/clock-double.d.ts +93 -0
  26. package/dist/clock-double.js +141 -0
  27. package/dist/clock-double.js.map +1 -0
  28. package/dist/clock.d.ts +39 -0
  29. package/dist/clock.js +34 -0
  30. package/dist/clock.js.map +1 -0
  31. package/dist/disposable-stack.d.ts +59 -0
  32. package/dist/disposable-stack.js +155 -0
  33. package/dist/disposable-stack.js.map +1 -0
  34. package/dist/docChrome.d.ts +260 -0
  35. package/dist/docChrome.js +361 -0
  36. package/dist/docChrome.js.map +1 -0
  37. package/dist/eslint-plugin/config.d.mts +193 -0
  38. package/dist/eslint-plugin/index.d.mts +2 -0
  39. package/dist/eslint-plugin/plugin.d.mts +87 -0
  40. package/dist/eslint-plugin/rules/blank-line-after-open-brace.d.mts +19 -0
  41. package/dist/eslint-plugin/rules/comment-style.d.mts +15 -0
  42. package/dist/eslint-plugin/rules/enforce-node-protocol.d.mts +18 -0
  43. package/dist/eslint-plugin/rules/paren-comparisons-in-logical.d.mts +15 -0
  44. package/dist/eslint-plugin/rules/split-type-imports.d.mts +16 -0
  45. package/dist/featureOptions-docs.d.ts +96 -0
  46. package/dist/featureOptions-docs.js +193 -0
  47. package/dist/featureOptions-docs.js.map +1 -0
  48. package/dist/featureOptions.d.ts +674 -0
  49. package/dist/featureOptions.js +870 -0
  50. package/dist/featureOptions.js.map +1 -0
  51. package/dist/ffmpeg/codecs.d.ts +256 -72
  52. package/dist/ffmpeg/codecs.js +477 -262
  53. package/dist/ffmpeg/codecs.js.map +1 -1
  54. package/dist/ffmpeg/dgram-util.d.ts +46 -0
  55. package/dist/ffmpeg/dgram-util.js +38 -0
  56. package/dist/ffmpeg/dgram-util.js.map +1 -0
  57. package/dist/ffmpeg/exec.d.ts +83 -64
  58. package/dist/ffmpeg/exec.js +77 -86
  59. package/dist/ffmpeg/exec.js.map +1 -1
  60. package/dist/ffmpeg/fmp4.d.ts +49 -2
  61. package/dist/ffmpeg/fmp4.js +47 -11
  62. package/dist/ffmpeg/fmp4.js.map +1 -1
  63. package/dist/ffmpeg/hap-enums.d.ts +214 -0
  64. package/dist/ffmpeg/hap-enums.js +92 -0
  65. package/dist/ffmpeg/hap-enums.js.map +1 -0
  66. package/dist/ffmpeg/index.d.ts +16 -9
  67. package/dist/ffmpeg/index.js +6 -0
  68. package/dist/ffmpeg/index.js.map +1 -1
  69. package/dist/ffmpeg/mp4-assembler.d.ts +161 -0
  70. package/dist/ffmpeg/mp4-assembler.js +424 -0
  71. package/dist/ffmpeg/mp4-assembler.js.map +1 -0
  72. package/dist/ffmpeg/mp4-parser.d.ts +94 -0
  73. package/dist/ffmpeg/mp4-parser.js +130 -0
  74. package/dist/ffmpeg/mp4-parser.js.map +1 -0
  75. package/dist/ffmpeg/options.d.ts +62 -149
  76. package/dist/ffmpeg/options.js +608 -499
  77. package/dist/ffmpeg/options.js.map +1 -1
  78. package/dist/ffmpeg/process.d.ts +142 -96
  79. package/dist/ffmpeg/process.js +406 -278
  80. package/dist/ffmpeg/process.js.map +1 -1
  81. package/dist/ffmpeg/record.d.ts +325 -186
  82. package/dist/ffmpeg/record.js +418 -565
  83. package/dist/ffmpeg/record.js.map +1 -1
  84. package/dist/ffmpeg/recording-process-double.d.ts +157 -0
  85. package/dist/ffmpeg/recording-process-double.js +190 -0
  86. package/dist/ffmpeg/recording-process-double.js.map +1 -0
  87. package/dist/ffmpeg/rtp-parser.d.ts +70 -0
  88. package/dist/ffmpeg/rtp-parser.js +77 -0
  89. package/dist/ffmpeg/rtp-parser.js.map +1 -0
  90. package/dist/ffmpeg/rtp.d.ts +198 -141
  91. package/dist/ffmpeg/rtp.js +474 -251
  92. package/dist/ffmpeg/rtp.js.map +1 -1
  93. package/dist/ffmpeg/settings.d.ts +5 -2
  94. package/dist/ffmpeg/settings.js +20 -5
  95. package/dist/ffmpeg/settings.js.map +1 -1
  96. package/dist/ffmpeg/stream.d.ts +57 -107
  97. package/dist/ffmpeg/stream.js +121 -150
  98. package/dist/ffmpeg/stream.js.map +1 -1
  99. package/dist/formatters.d.ts +106 -0
  100. package/dist/formatters.js +174 -0
  101. package/dist/formatters.js.map +1 -0
  102. package/dist/homebridge-enums.d.ts +30 -0
  103. package/dist/homebridge-enums.js +17 -0
  104. package/dist/homebridge-enums.js.map +1 -0
  105. package/dist/index.d.ts +13 -6
  106. package/dist/index.js +8 -2
  107. package/dist/index.js.map +1 -1
  108. package/dist/logclient/auth.d.ts +114 -0
  109. package/dist/logclient/auth.js +199 -0
  110. package/dist/logclient/auth.js.map +1 -0
  111. package/dist/logclient/cli-run.d.ts +76 -0
  112. package/dist/logclient/cli-run.js +639 -0
  113. package/dist/logclient/cli-run.js.map +1 -0
  114. package/dist/logclient/cli.d.ts +3 -0
  115. package/dist/logclient/cli.js +97 -0
  116. package/dist/logclient/cli.js.map +1 -0
  117. package/dist/logclient/client.d.ts +145 -0
  118. package/dist/logclient/client.js +600 -0
  119. package/dist/logclient/client.js.map +1 -0
  120. package/dist/logclient/config.d.ts +173 -0
  121. package/dist/logclient/config.js +199 -0
  122. package/dist/logclient/config.js.map +1 -0
  123. package/dist/logclient/endpoints.d.ts +54 -0
  124. package/dist/logclient/endpoints.js +73 -0
  125. package/dist/logclient/endpoints.js.map +1 -0
  126. package/dist/logclient/filter.d.ts +45 -0
  127. package/dist/logclient/filter.js +51 -0
  128. package/dist/logclient/filter.js.map +1 -0
  129. package/dist/logclient/frame.d.ts +93 -0
  130. package/dist/logclient/frame.js +203 -0
  131. package/dist/logclient/frame.js.map +1 -0
  132. package/dist/logclient/index.d.ts +31 -0
  133. package/dist/logclient/index.js +12 -0
  134. package/dist/logclient/index.js.map +1 -0
  135. package/dist/logclient/parser.d.ts +211 -0
  136. package/dist/logclient/parser.js +393 -0
  137. package/dist/logclient/parser.js.map +1 -0
  138. package/dist/logclient/rest.d.ts +41 -0
  139. package/dist/logclient/rest.js +111 -0
  140. package/dist/logclient/rest.js.map +1 -0
  141. package/dist/logclient/settings.d.ts +15 -0
  142. package/dist/logclient/settings.js +64 -0
  143. package/dist/logclient/settings.js.map +1 -0
  144. package/dist/logclient/socket-double.d.ts +201 -0
  145. package/dist/logclient/socket-double.js +384 -0
  146. package/dist/logclient/socket-double.js.map +1 -0
  147. package/dist/logclient/socket.d.ts +257 -0
  148. package/dist/logclient/socket.js +620 -0
  149. package/dist/logclient/socket.js.map +1 -0
  150. package/dist/logclient/stitch.d.ts +83 -0
  151. package/dist/logclient/stitch.js +146 -0
  152. package/dist/logclient/stitch.js.map +1 -0
  153. package/dist/logclient/time-expression.d.ts +42 -0
  154. package/dist/logclient/time-expression.js +181 -0
  155. package/dist/logclient/time-expression.js.map +1 -0
  156. package/dist/logclient/time-window.d.ts +38 -0
  157. package/dist/logclient/time-window.js +53 -0
  158. package/dist/logclient/time-window.js.map +1 -0
  159. package/dist/logclient/types.d.ts +107 -0
  160. package/dist/logclient/types.js +6 -0
  161. package/dist/logclient/types.js.map +1 -0
  162. package/dist/mqttClient.d.ts +287 -0
  163. package/dist/mqttClient.js +433 -0
  164. package/dist/mqttClient.js.map +1 -0
  165. package/dist/service.d.ts +64 -15
  166. package/dist/service.js +93 -66
  167. package/dist/service.js.map +1 -1
  168. package/dist/ui/featureOptions.js +870 -0
  169. package/dist/ui/featureOptions.js.map +1 -0
  170. package/dist/ui/formatters.js +174 -0
  171. package/dist/ui/formatters.js.map +1 -0
  172. package/dist/ui/pluginConfigSession.mjs +141 -0
  173. package/dist/ui/webUi-featureOptions/categoryState.mjs +135 -0
  174. package/dist/ui/webUi-featureOptions/effects/keyboard.mjs +61 -0
  175. package/dist/ui/webUi-featureOptions/effects/persist.mjs +226 -0
  176. package/dist/ui/webUi-featureOptions/effects/theme.mjs +398 -0
  177. package/dist/ui/webUi-featureOptions/effects/tokens.mjs +152 -0
  178. package/dist/ui/webUi-featureOptions/rendering.mjs +431 -0
  179. package/dist/ui/webUi-featureOptions/selectors.mjs +360 -0
  180. package/dist/ui/webUi-featureOptions/state.mjs +319 -0
  181. package/dist/ui/webUi-featureOptions/store.mjs +181 -0
  182. package/dist/ui/webUi-featureOptions/utils.mjs +213 -0
  183. package/dist/ui/webUi-featureOptions/views/connectionError.mjs +152 -0
  184. package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +80 -0
  185. package/dist/ui/webUi-featureOptions/views/header.mjs +77 -0
  186. package/dist/ui/webUi-featureOptions/views/nav.mjs +341 -0
  187. package/dist/ui/webUi-featureOptions/views/options.mjs +521 -0
  188. package/dist/ui/webUi-featureOptions/views/search.mjs +395 -0
  189. package/dist/ui/webUi-featureOptions.mjs +702 -0
  190. package/dist/ui/webUi.mjs +225 -88
  191. package/dist/util.d.ts +679 -45
  192. package/dist/util.js +830 -77
  193. package/dist/util.js.map +1 -1
  194. package/package.json +33 -15
  195. package/build/eslint-rules.mjs +0 -511
  196. package/dist/featureoptions.d.ts +0 -264
  197. package/dist/featureoptions.js +0 -480
  198. package/dist/featureoptions.js.map +0 -1
  199. package/dist/mqttclient.d.ts +0 -178
  200. package/dist/mqttclient.js +0 -310
  201. package/dist/mqttclient.js.map +0 -1
  202. package/dist/ui/featureoptions.js +0 -480
  203. package/dist/ui/featureoptions.js.map +0 -1
  204. package/dist/ui/webUi-featureoptions.mjs +0 -3765
package/README.md CHANGED
@@ -14,7 +14,7 @@ The design decisions are driven by my own needs as I continue to create, evolve,
14
14
 
15
15
  ### Features
16
16
 
17
- - **Feature options*.* Feature options are a hierarchical configuration system and matching webUI that allows users to set global defaults and override them at a granular level, enabling easier mass-customization of capabilities. For plugins that can potentially enumerate dozens of devices, this comes in quite handy so you don't need to configure each and every device, and instead you can focus on the exceptions.
17
+ - **Feature options.** Feature options are a hierarchical configuration system and matching webUI that allows users to set global defaults and override them at a granular level, enabling easier mass-customization of capabilities. For plugins that can potentially enumerate dozens of devices, this comes in quite handy so you don't need to configure each and every device, and instead you can focus on the exceptions.
18
18
 
19
19
  - **Configuration webUI.** This a rich, custom webUI for enumerating all the devices a plugin knows about, and configuring feature options.
20
20
 
@@ -22,11 +22,139 @@ The design decisions are driven by my own needs as I continue to create, evolve,
22
22
 
23
23
  - **MQTT client.** Building in MQTT client capabilities is made easier through a set of utilities that allow you to easily publish and subscribe to events.
24
24
 
25
+ - **`hblog` log client.** A zero-dependency tool for tailing and querying a `homebridge-config-ui-x` log, usable both as the `hblog` command-line bin and as the importable `HomebridgeLogClient` API. See [Log Client (`hblog`)](#log-client-hblog) below.
26
+
27
+ - **Plugin tooling.** A `homebridge-plugin-utils` command-line tool that mirrors the compiled webUI into your plugin under a content-hashed folder (so the browser never serves a stale copy after a rebuild) and regenerates your Feature Options reference from its catalog (so the docs can't drift). See [Plugin Tooling (`prepare-ui` and `prepare-docs`)](#plugin-tooling-prepare-ui-and-prepare-docs) below.
28
+
25
29
  - **And more...**
26
30
 
27
31
  ## Documentation
28
32
 
29
- Documentation and examples for using this library to simplify and enhance Homebridge plugin development, especially camera-related plugins, is [available here](https://github.com/hjdhjd/homebridge-plugin-utils/blob/main/docs/README.md). Additionally, if you'd like to see this library being used n a well-documented, real-world example, please take a good look at my [homebridge-unifi-protect](https://github.com/hjdhjd/homebridge-unifi-protect) project. It relies heavily on this library for much of the functionality it provides.
33
+ Documentation and examples for using this library to simplify and enhance Homebridge plugin development, especially camera-related plugins, is [available here](https://github.com/hjdhjd/homebridge-plugin-utils/blob/main/docs/README.md). Additionally, if you'd like to see this library being used in a well-documented, real-world example, please take a good look at my [homebridge-unifi-protect](https://github.com/hjdhjd/homebridge-unifi-protect) project. It relies heavily on this library for much of the functionality it provides.
34
+
35
+ ## Log Client (`hblog`)
36
+
37
+ `hblog` is a zero-dependency tool for tailing and querying the log of a [homebridge-config-ui-x](https://github.com/homebridge/homebridge-config-ui-x) instance. It connects to the web UI, authenticates, and either live-tails the log over the UI's Socket.IO stream or pulls historical lines over REST. It is meant for plugin development and debugging.
38
+
39
+ It is available two ways: as the `hblog` command-line bin, and as the importable `HomebridgeLogClient` API.
40
+
41
+ ### Command line
42
+
43
+ ```
44
+ hblog [filters] [options]
45
+
46
+ Connection:
47
+ --host <host> The homebridge-config-ui-x host (default: localhost).
48
+ --port <port> The server port (default: 8581).
49
+ --tls Connect over TLS (https/wss).
50
+ --user <username> The login username.
51
+ --pass <password> The login password.
52
+ --token <token> A pre-acquired bearer token (used verbatim).
53
+ --otp <code> A one-time passcode for a 2FA-enabled account.
54
+
55
+ Mode:
56
+ -f, --follow Live-tail the log (default).
57
+ -n, --lines <N> Retrieve the most recent N lines.
58
+ --all Retrieve the entire log (cannot be combined with -n).
59
+
60
+ Filters:
61
+ -p, --plugin <name> Only show lines from this plugin (repeatable).
62
+ -g, --grep <regex> Only show lines whose message matches this regular expression.
63
+ -l, --level <level> Only show lines at this level: debug, error, info, success, warn (repeatable).
64
+
65
+ Output:
66
+ --json Emit one JSON record per line (NDJSON).
67
+ --raw Emit raw lines with ANSI escapes preserved.
68
+ --no-color Strip ANSI escapes from the output.
69
+ --version Print the hblog version and exit.
70
+ -h, --help Print this help and exit.
71
+ ```
72
+
73
+ Bare `hblog` live-tails the log. `--follow` rides the UI's Socket.IO stream - it is cheap and incremental, includes a free ~500-line seed of recent history, and reconnects automatically through the Homebridge restarts you do while iterating. A deep `-n N` or `--all` falls back to a one-shot whole-file download, paid only when you explicitly ask for history beyond the seed. `history`/`--all` require the Homebridge log method to be file-backed (`file`/`native`); with `systemd`/`custom` there is no file to download, so use `--follow` instead.
74
+
75
+ Output discipline is pipe-friendly: log data goes to stdout (so `--json` NDJSON stays clean), diagnostics and warnings go to stderr, and a broken downstream pipe (`hblog -f | grep -m1 ...`) ends cleanly. Exit codes are `0` for success (including a clean Ctrl-C), `1` for a connection or authentication failure, and `2` for a usage error.
76
+
77
+ ### `~/.hblog.json`
78
+
79
+ Connection settings are resolved with the precedence flags > environment > config file > defaults. The environment variables are `HBLOG_HOST`, `HBLOG_PORT`, `HBLOG_USER`, `HBLOG_PASS`, `HBLOG_TOKEN`, and `HBLOG_OTP`. The optional config file lives at `~/.hblog.json` (override the path with `HBLOG_CONFIG`) and carries any subset of these keys:
80
+
81
+ ```json
82
+ {
83
+ "host": "localhost",
84
+ "port": 8581,
85
+ "tls": false,
86
+ "username": "admin",
87
+ "password": "your-password",
88
+ "token": "a-pre-acquired-bearer-token"
89
+ }
90
+ ```
91
+
92
+ A missing file is silent, a malformed file is a clear error, and unknown keys are ignored. The file has no `otp` key on purpose - a one-time passcode is single-use and time-bound, so it only ever comes from `--otp` or `HBLOG_OTP`.
93
+
94
+ > **Credential security.** `~/.hblog.json` may store a password or a long-lived token in plaintext. Restrict it to your account with `chmod 600 ~/.hblog.json`; `hblog` prints a one-line warning if the file is readable by group or other. Tokens are never logged, and any token embedded in a URL or error message is redacted before it is printed.
95
+
96
+ ### `FORCE_COLOR` and level filtering
97
+
98
+ In `homebridge-config-ui-x`, a log line's severity is conveyed only by its ANSI color, not by a textual label - and that color is usually absent when Homebridge runs under hb-service/systemd. Reliable `--level` filtering therefore requires color to be present: set `FORCE_COLOR=1` on the **Homebridge process** so it emits colored output. When a `--level` filter is active but the lines carry no color, `hblog` warns once and passes lines through unfiltered by level rather than producing silent empty output.
99
+
100
+ ### Programmatic use
101
+
102
+ ```ts
103
+ import { HomebridgeLogClient } from "homebridge-plugin-utils";
104
+
105
+ await using client = new HomebridgeLogClient({ credentials: { kind: "password", password: "secret", username: "admin" }, host: "localhost" });
106
+
107
+ await using stream = client.tail({ mode: "follow-history", quantity: 200 });
108
+
109
+ for await (const record of stream) {
110
+
111
+ process.stdout.write(record.raw + "\n");
112
+ }
113
+ ```
114
+
115
+ `HomebridgeLogClient` is `AsyncDisposable`: `await using` (or an early `break` out of the iteration) tears the underlying transport down with no leak. Its three channels - `history()`, `follow()`, and `tail()` - each return a `LogStream` of parsed `LogRecord`s. Filtering is consumer-composed via `createLogFilter` over any stream.
116
+
117
+ ## Plugin Tooling (`prepare-ui` and `prepare-docs`)
118
+
119
+ Installing this library also installs a `homebridge-plugin-utils` command-line tool that automates the two build steps every consuming plugin needs: mirroring the compiled webUI into the plugin, and keeping the Feature Options reference in sync with the catalog. Both are meant to run from a plugin's `build` or `prepublishOnly` script so neither artifact can drift from the source.
120
+
121
+ ```
122
+ Usage: homebridge-plugin-utils <command> [options]
123
+
124
+ Commands:
125
+ prepare-ui <destination> Mirror HBPU's webUI into the plugin's lib directory.
126
+ prepare-docs <catalog-module> [--doc <path>] Generate the Feature Options reference into the plugin's docs.
127
+ ```
128
+
129
+ - **`prepare-ui <destination>`** mirrors this library's compiled browser-side webUI into your plugin's UI directory (typically `homebridge-ui/public/lib`) under a content-hashed, version-named subfolder. Because the folder name changes whenever its contents change, the browser's HTTP cache invalidates structurally - you never have to chase a stale cached copy after a rebuild. The run is idempotent and sweeps away the previous build's subfolder in the same pass, while leaving any non-versioned files in the destination untouched.
130
+
131
+ - **`prepare-docs <catalog-module> [--doc <path>]`** regenerates your plugin's Feature Options reference straight from its options catalog, splicing it into a marked region of the target document - `docs/FeatureOptions.md` by default, or the `--doc` path you pass for a plugin that ships its reference elsewhere. Running it on every build keeps the published reference from drifting away from the options you actually ship.
132
+
133
+ ## Lint Configuration
134
+
135
+ Plugins that extend `homebridge-plugin-utils/eslint` automatically inherit a small set of in-house ESLint rules under the `@hjdhjd` namespace. Most are stylistic warnings; one (`@hjdhjd/comment-style`) is an enforcement rule wired at error severity to keep comments grep-able and rendering-stable across editors, terminals, diff tools, and review UIs.
136
+
137
+ ### `@hjdhjd/comment-style`
138
+
139
+ The rule walks every `//` and `/* */` comment in a single pass and reports four classes of drift. Each class autofixes mechanically under `eslint --fix`.
140
+
141
+ | Group | Pattern | Autofix |
142
+ |---|---|---|
143
+ | A | Unicode glyphs with direct ASCII equivalents: left-right arrow (U+2194), rightwards arrow (U+2192), leftwards arrow (U+2190), less-than-or-equal (U+2264), greater-than-or-equal (U+2265), plus-minus (U+00B1) | Substituted with `<->`, `->`, `<-`, `<=`, `>=`, `+/-` respectively |
144
+ | B | Em-dash (U+2014) | Substituted with a regular hyphen `-` |
145
+ | C | Decorative banner separators in line comments: runs of four or more `=`, `-`, or `#` on a line by themselves | The entire offending source line is removed, including its trailing newline |
146
+ | D | Box-drawing characters: the full Unicode Box Drawing block (U+2500..U+257F), covering both single-line and double-line forms | The offending characters are stripped from the comment |
147
+
148
+ The rule is comment-scoped by construction. It walks the AST's comment list and cannot touch string literals, template positions, or identifier names, so a webUI label that legitimately renders an arrow to the user, or a test fixture exercising em-dash rendering, is never affected.
149
+
150
+ For the rare case where a comment must contain one of these characters - documentation that quotes a banned glyph by name, a unit test for the rule itself, or a generated comment that references an external character set - use ESLint's standard inline disable directive on the line above:
151
+
152
+ ```js
153
+ // eslint-disable-next-line @hjdhjd/comment-style
154
+ // Renders the user-visible right-arrow glyph: →
155
+ ```
156
+
157
+ For multiple consecutive lines, wrap the region with `/* eslint-disable @hjdhjd/comment-style */` and `/* eslint-enable @hjdhjd/comment-style */` block directives.
30
158
 
31
159
  ## Plugin Development Dashboard
32
160
  This is mostly of interest to the true developer nerds amongst us.
@@ -0,0 +1,164 @@
1
+ # `@hjdhjd` ESLint plugin
2
+
3
+ A self-contained ESLint flat-config plugin bundled inside `homebridge-plugin-utils`. Provides five custom rules and an opinionated flat-config builder
4
+ tuned for Homebridge plugin development - TypeScript backend, JavaScript build tooling, browser-side webUI - all in one consistent rule surface.
5
+
6
+ ## Quick start
7
+
8
+ The repo's root `eslint.config.mjs` consumes the plugin through its config helper:
9
+
10
+ ```js
11
+ import hbPluginUtils from "./build/eslint-plugin/index.mjs";
12
+
13
+ export default hbPluginUtils({
14
+
15
+ ts: [ "src/**/*.ts" ],
16
+ js: [ "build/**/*.mjs", "eslint.config.mjs" ],
17
+ ui: [ "ui/**/*.@(js|mjs)" ]
18
+ });
19
+ ```
20
+
21
+ That single call returns a complete flat-config array: TypeScript rules scoped to `.ts` files, JavaScript rules scoped to `.mjs` files, the common
22
+ `@hjdhjd/*` and `@stylistic/*` rule set applied everywhere, and webUI globals scoped to UI files.
23
+
24
+ ## File layout
25
+
26
+ ```
27
+ build/eslint-plugin/
28
+ ├── README.md # this file
29
+ ├── index.mjs # public surface: re-exports plugin + config
30
+ ├── plugin.mjs # the ESLint plugin object: { meta, rules }
31
+ ├── config.mjs # opinionated flat-config builder and rule presets
32
+ ├── test-setup.mjs # shared RuleTester scaffold for the rule test suites
33
+ └── rules/
34
+ ├── blank-line-after-open-brace.mjs # @hjdhjd/blank-line-after-open-brace
35
+ ├── blank-line-after-open-brace.test.mjs # co-located RuleTester suite
36
+ ├── comment-style.mjs # @hjdhjd/comment-style
37
+ ├── comment-style.test.mjs # co-located RuleTester suite
38
+ ├── enforce-node-protocol.mjs # @hjdhjd/enforce-node-protocol
39
+ ├── enforce-node-protocol.test.mjs # co-located RuleTester suite
40
+ ├── paren-comparisons-in-logical.mjs # @hjdhjd/paren-comparisons-in-logical
41
+ ├── paren-comparisons-in-logical.test.mjs # co-located RuleTester suite
42
+ ├── split-type-imports.mjs # @hjdhjd/split-type-imports
43
+ └── split-type-imports.test.mjs # co-located RuleTester suite
44
+ ```
45
+
46
+ Each `rules/*.mjs` file is the full unit for one rule: copyright header, module-level constants, pure helpers, rule header comment, rule object, default
47
+ export. Tests for a rule live in `<rule-name>.test.mjs` alongside it. The two layers above the rules - `plugin.mjs` (what the plugin IS) and `config.mjs`
48
+ (what the plugin RECOMMENDS) - are cleanly separated so a consumer can use the rules without inheriting the opinions, or use the opinions without
49
+ reaching into rule internals.
50
+
51
+ ## Public surface
52
+
53
+ `index.mjs` exposes the package's full API as re-exports:
54
+
55
+ | Export | From | What it is |
56
+ |---|---|---|
57
+ | `default` / `config` | `config.mjs` | The flat-config builder function. Takes file-glob options, returns an array suitable for `export default` in `eslint.config.mjs`. |
58
+ | `plugin` | `plugin.mjs` | The ESLint plugin object - `{ meta, rules }`. Drop into a flat config block under `plugins["@hjdhjd"]`. |
59
+ | `plugins` | `config.mjs` | The composed plugin namespace map - `@hjdhjd`, `@stylistic`, `@typescript-eslint`. |
60
+ | `commonRules` | `config.mjs` | Rule preset applied to every linted file regardless of language. |
61
+ | `tsRules` | `config.mjs` | TypeScript-specific rule preset. |
62
+ | `jsRules` | `config.mjs` | JavaScript-specific rule preset. |
63
+ | `globalsUi` | `config.mjs` | Browser-environment globals dictionary for webUI files. |
64
+
65
+ Every named export has JSDoc on its source declaration.
66
+
67
+ ## The rules
68
+
69
+ Each rule is described in detail by its own file's header comment. One-line summaries:
70
+
71
+ | Rule | Purpose |
72
+ |---|---|
73
+ | `@hjdhjd/blank-line-after-open-brace` | Require a blank line after an opening brace when the brace is followed by a newline and the next line carries non-whitespace content. Covers block statements, class bodies, object expressions, and TypeScript interface/type-literal bodies. |
74
+ | `@hjdhjd/comment-style` | Enforce ASCII-first comment style. Substitutes Unicode arrows/comparison glyphs/em-dash with ASCII equivalents, removes decorative banner separators, and strips characters from the Unicode Box Drawing block. |
75
+ | `@hjdhjd/enforce-node-protocol` | Require the `node:` protocol prefix on every reference to a Node.js built-in module. |
76
+ | `@hjdhjd/paren-comparisons-in-logical` | Require parentheses around any comparison operand that is a direct child of a compound `&&` or `||` expression. |
77
+ | `@hjdhjd/split-type-imports` | Require type imports and re-exports to live in declaration-level `import type` / `export type` statements rather than as inline specifier-level `type` qualifiers. Pairs with `@typescript-eslint/consistent-type-imports` configured with `fixStyle: "separate-type-imports"` to express the split-form policy as a single source of truth. |
78
+
79
+ ## Plugin shape
80
+
81
+ `plugin.mjs` exports the standard ESLint plugin shape:
82
+
83
+ ```js
84
+ {
85
+ meta: { name: "@hjdhjd/eslint-rules", version: <package.json version> },
86
+ rules: { /* the five rules above */ }
87
+ }
88
+ ```
89
+
90
+ The version is sourced from the host package's `package.json` via the ECMAScript `import attributes` syntax (`with { type: "json" }`), so it auto-syncs
91
+ with the package version on release.
92
+
93
+ ## Architecture conventions
94
+
95
+ The plugin follows a few intentional conventions that make it predictable to extend and to read:
96
+
97
+ - **One rule per file under `rules/`.** Each rule file is self-contained: its constants, pure helpers, rule header, rule object, and default export all
98
+ live in one place. Helpers are private to the rule's file - cross-rule shared utilities are deliberately avoided to prevent premature abstractions.
99
+
100
+ - **Pure helpers at module scope, taking dependencies as parameters.** A helper that needs `sourceCode` takes it as its first argument rather than
101
+ closing over it. The function signature tells the truth about what the helper uses, and the helpers are reachable for unit-testing in isolation if
102
+ needed.
103
+
104
+ - **The rule's `create(context)` body stays minimal.** It reads options from `context`, sets up the thin reporter wrapper, and declares its visitors.
105
+ All real logic lives in the module-level helpers above.
106
+
107
+ - **Three-part rule header comment on every rule.** What the rule enforces (one sentence), what cases it covers (enumerated), and any non-obvious
108
+ design decisions. Open any rule file and the first comment block tells you everything you need to know.
109
+
110
+ - **Tests co-located with rules.** A rule's `RuleTester` suite lives in `<rule-name>.test.mjs` next to the rule file. Matches the convention used
111
+ throughout `src/` in the host package.
112
+
113
+ - **Plugin meta in standard ESLint shape.** `meta.name` plus `meta.version` so the plugin is identifiable to ESLint's cache invalidation, the lint
114
+ output, and any tooling that inspects loaded plugins.
115
+
116
+ ## Adding a new rule
117
+
118
+ 1. Create `rules/<new-rule-name>.mjs` with:
119
+ - Copyright header.
120
+ - Module-level constants and pure helpers for the rule.
121
+ - The three-part rule header comment.
122
+ - The rule object as a `const`, with default export.
123
+
124
+ 2. Create `rules/<new-rule-name>.test.mjs` co-located alongside, importing `RuleTester` from the shared `../test-setup.mjs` scaffold:
125
+
126
+ ```js
127
+ import { RuleTester } from "../test-setup.mjs";
128
+ import rule from "./<new-rule-name>.mjs";
129
+
130
+ const ruleTester = new RuleTester(/* optional languageOptions */);
131
+
132
+ ruleTester.run("<new-rule-name>", rule, { invalid: [ /* ... */ ], valid: [ /* ... */ ] });
133
+ ```
134
+
135
+ Cover every case the rule's header documents under "Cases covered" plus negative passthrough cases that prove the canonical form is ignored.
136
+
137
+ 3. In `plugin.mjs`, add one `import` line and one entry to the `rules` map (alphabetical).
138
+
139
+ 4. If the rule should be enabled by default, add one line to the appropriate preset in `config.mjs`: `commonRules` (applies to every linted file), `tsRules` (TypeScript-only), or `jsRules` (JavaScript-only).
140
+
141
+ That's the whole motion. No other files need to change.
142
+
143
+ ## Composing with downstream consumers
144
+
145
+ The `config()` helper is the primary API for consumers who want the full opinionated stack. For consumers who want only the plugin's rules without
146
+ the opinions, import `plugin` from `index.mjs` and wire it into their own flat config directly:
147
+
148
+ ```js
149
+ import { plugin } from "homebridge-plugin-utils/build/eslint-plugin/index.mjs";
150
+
151
+ export default [{
152
+
153
+ plugins: { "@hjdhjd": plugin },
154
+
155
+ rules: {
156
+
157
+ "@hjdhjd/comment-style": "error",
158
+ "@hjdhjd/split-type-imports": "warn"
159
+ }
160
+ }];
161
+ ```
162
+
163
+ The plugin is namespace-agnostic - register it under any name you like in `plugins:` and reference the rules under that prefix. The `@hjdhjd/` namespace
164
+ in `commonRules` is wired to match the bundled defaults; if you re-namespace the plugin in your own config, you'll also need to remap the rule names.
@@ -0,0 +1,308 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * config.mjs: Opinionated ESLint flat-config builder. Composes the `@hjdhjd` plugin together with the project's rule presets for TypeScript, JavaScript,
4
+ * and browser-UI files.
5
+ */
6
+ import eslintJs from "@eslint/js";
7
+ import plugin from "./plugin.mjs";
8
+ import stylistic from "@stylistic/eslint-plugin";
9
+ import ts from "typescript-eslint";
10
+
11
+ // Extract rules by name from a typescript-eslint config array. Throws if the named entry is missing so that a typescript-eslint version bump that renames
12
+ // config entries fails loudly rather than silently dropping rules.
13
+ function configRules(configs, name) {
14
+
15
+ const entry = configs.find((c) => c.name === name);
16
+
17
+ if(!entry) {
18
+
19
+ throw new Error("typescript-eslint config entry not found: " + name);
20
+ }
21
+
22
+ return entry.rules;
23
+ }
24
+
25
+ /**
26
+ * The ESLint plugin namespace map used by the composed configuration: `@hjdhjd` (this package's rules), `@stylistic` (the official stylistic-rules
27
+ * plugin), and `@typescript-eslint`. Drop this object into the `plugins` slot of a flat config block to register every plugin namespace this preset
28
+ * composes at once.
29
+ *
30
+ * The type annotation is deliberately loose since consumers that read the named `plugins` export do their own narrowing before touching rule entries.
31
+ * It's annotated explicitly so the emitted type stays portable - the raw inference picks up typescript-eslint's exported `CompatiblePlugin` shape,
32
+ * and the `tsconfig.eslint-plugin.json` declaration-only build (TS2742) refuses to serialize it into the `.d.ts` without citing a deep module path.
33
+ *
34
+ * @type {Record<string, { meta?: { name: string; version?: string }; rules?: Record<string, unknown> }>}
35
+ */
36
+ const plugins = {
37
+
38
+ "@hjdhjd": plugin,
39
+ "@stylistic": stylistic,
40
+ "@typescript-eslint": ts.plugin
41
+ };
42
+
43
+ /**
44
+ * Rule preset for TypeScript source files. Builds on `typescript-eslint`'s strict and stylistic type-checked configs, then layers project-specific
45
+ * overrides: type-aware lint rules (`await-thenable`, `no-floating-promises`, `prefer-nullish-coalescing`, etc.) at warn level, explicit return-type and
46
+ * module-boundary requirements, and `@stylistic/member-delimiter-style`. Disables the JavaScript-recommended versions of rules that have TypeScript-aware
47
+ * equivalents to avoid double-flagging.
48
+ *
49
+ * Deliberately omits `@typescript-eslint/promise-function-async` and forces `@typescript-eslint/require-await` (and the base `require-await`) off. The pair
50
+ * encodes a tight coupling between Promise return types and the `async` keyword that predates the Disposable protocol: `[Symbol.asyncDispose]()` must be declared
51
+ * `async ...(): Promise<void>` to satisfy `await using` even when the body is synchronous, and identity-preserving Promise pass-throughs (e.g., `markHandled` in
52
+ * `src/util.ts`) cannot be marked `async` without wrapping the return in a fresh chain and breaking reference equality. Encoding the stance at the preset level
53
+ * rather than scattering per-site `eslint-disable` directives is the SSOT.
54
+ *
55
+ * Spread into the `rules:` slot of a flat config block scoped to `.ts` files.
56
+ */
57
+ const tsRules = {
58
+
59
+ ...configRules(ts.configs.strictTypeChecked, "typescript-eslint/strict-type-checked"),
60
+ ...configRules(ts.configs.stylisticTypeChecked, "typescript-eslint/stylistic-type-checked"),
61
+ "@stylistic/member-delimiter-style": "warn",
62
+ "@typescript-eslint/await-thenable": "warn",
63
+ "@typescript-eslint/consistent-type-imports": [ "warn", { "fixStyle": "separate-type-imports", "prefer": "type-imports" } ],
64
+ "@typescript-eslint/explicit-function-return-type": "warn",
65
+ "@typescript-eslint/explicit-module-boundary-types": "warn",
66
+ "@typescript-eslint/no-confusing-void-expression": [ "error", { "ignoreArrowShorthand": true, "ignoreVoidOperator": true,
67
+ "ignoreVoidReturningFunctions": true } ],
68
+ "@typescript-eslint/no-explicit-any": "warn",
69
+ "@typescript-eslint/no-floating-promises": [ "warn", { "ignoreIIFE": true } ],
70
+ "@typescript-eslint/no-non-null-assertion": "warn",
71
+ "@typescript-eslint/no-unnecessary-condition": "warn",
72
+ "@typescript-eslint/no-unused-expressions": "warn",
73
+ "@typescript-eslint/no-unused-vars": [ "warn", { "argsIgnorePattern": "^_", "caughtErrors": "all", "caughtErrorsIgnorePattern": "^_", "varsIgnorePattern": "^_" } ],
74
+ "@typescript-eslint/prefer-nullish-coalescing": "warn",
75
+ "@typescript-eslint/require-await": "off",
76
+ "no-dupe-class-members": "off",
77
+ "no-redeclare": "off",
78
+ "no-undef": "off",
79
+ "no-unused-expressions": "off",
80
+ "no-unused-vars": "off",
81
+ "require-await": "off"
82
+ };
83
+
84
+ /**
85
+ * Rule preset for JavaScript source files. Starts from `typescript-eslint`'s `disableTypeChecked` set (so type-aware rules don't fire on plain JS), then
86
+ * re-enables `no-unused-vars` with the underscore-prefix ignore pattern. The `require-await` rule is left disabled here for the same reason the TypeScript
87
+ * preset omits its pair - see the omission paragraph in the {@link tsRules} JSDoc.
88
+ *
89
+ * Spread into the `rules:` slot of a flat config block scoped to `.js` / `.mjs` files.
90
+ */
91
+ const jsRules = {
92
+
93
+ ...ts.configs.disableTypeChecked.rules,
94
+
95
+ // Restates the "off" value that `disableTypeChecked` already assigns to this rule.
96
+ "@typescript-eslint/no-floating-promises": "off",
97
+ "no-unused-vars": [ "warn", { "argsIgnorePattern": "^_", "caughtErrors": "all", "caughtErrorsIgnorePattern": "^_", "varsIgnorePattern": "^_" } ]
98
+ };
99
+
100
+ /**
101
+ * Rule preset applied to every linted file regardless of language. Disables the base ESLint rules that are redundant once TypeScript's own compiler
102
+ * and syntax already catch the same errors, mirroring the `typescript-eslint` compatibility overlay it spreads in; the enabled ESLint-recommended
103
+ * baseline itself comes from the separate `eslintJs.configs.recommended` block pushed in {@link config}. Layers on top of that the full `@hjdhjd/*`
104
+ * rule set, the `@stylistic/*` whitespace and formatting rules, and the project's opinionated `sort-imports` / `sort-keys` / `quotes` / `eqeqeq` /
105
+ * `curly` constraints.
106
+ *
107
+ * Spread into the `rules:` slot of the all-files block of a flat config.
108
+ */
109
+ const commonRules = {
110
+
111
+ ...ts.configs.eslintRecommended.rules,
112
+ "@hjdhjd/blank-line-after-open-brace": "warn",
113
+ "@hjdhjd/comment-style": "error",
114
+ "@hjdhjd/enforce-node-protocol": "warn",
115
+ "@hjdhjd/paren-comparisons-in-logical": "warn",
116
+ "@hjdhjd/split-type-imports": "warn",
117
+ "@stylistic/array-bracket-spacing": [ "warn", "always", { "arraysInArrays": true, "objectsInArrays": true, "singleValue": false } ],
118
+ "@stylistic/block-spacing": "warn",
119
+ "@stylistic/brace-style": [ "warn", "1tbs", { "allowSingleLine": true } ],
120
+ "@stylistic/comma-dangle": "warn",
121
+ "@stylistic/eol-last": [ "warn", "always" ],
122
+ "@stylistic/generator-star-spacing": "warn",
123
+ "@stylistic/implicit-arrow-linebreak": "warn",
124
+ "@stylistic/indent": [ "warn", 2, { "SwitchCase": 1 } ],
125
+ "@stylistic/keyword-spacing": [ "warn",
126
+ { "overrides": { "for": { "after": false }, "if": { "after": false }, "switch": { "after": false }, "while": { "after": false } } } ],
127
+ "@stylistic/linebreak-style": [ "warn", "unix" ],
128
+ "@stylistic/lines-between-class-members": [ "warn", "always", { "exceptAfterSingleLine": true } ],
129
+ "@stylistic/max-len": [ "warn", 170 ],
130
+ "@stylistic/no-tabs": "warn",
131
+ "@stylistic/no-trailing-spaces": "warn",
132
+ "@stylistic/operator-linebreak": [ "warn", "after", { "overrides": { ":": "after", "?": "after" } } ],
133
+ "@stylistic/padding-line-between-statements": [ "warn",
134
+
135
+ // Require a blank line before every statement type in next.
136
+ { "blankLine": "always", "next": [ "break", "case", "class", "continue", "default", "export", "for", "function", "if", "import", "return" ], "prev": "*" },
137
+
138
+ // Require blank lines after every statement type in prev.
139
+ { "blankLine": "always", "next": "*", "prev": [ "const", "directive", "let", "var" ] },
140
+
141
+ // Multiple sequential case declarations may be grouped together.
142
+ { "blankLine": "any", "next": [ "case", "default" ], "prev": [ "case", "default" ] },
143
+
144
+ // Multiple sequential variable declarations may be grouped together.
145
+ { "blankLine": "any", "next": [ "const", "let", "var" ], "prev": [ "const", "let", "var" ] },
146
+
147
+ // Multiple sequential export declarations may be grouped together.
148
+ { "blankLine": "any", "next": "export", "prev": "export" },
149
+
150
+ // Multiple sequential import declarations must be grouped together.
151
+ { "blankLine": "never", "next": "import", "prev": "import" },
152
+
153
+ // Multiple sequential directive prologues must be grouped together.
154
+ { "blankLine": "never", "next": "directive", "prev": "directive" }
155
+ ],
156
+ "@stylistic/semi": [ "warn", "always" ],
157
+ "@stylistic/space-before-function-paren": [ "warn", { "anonymous": "never", "asyncArrow": "always", "catch": "never", "named": "never" } ],
158
+ "@stylistic/space-in-parens": "warn",
159
+ "@stylistic/space-infix-ops": "warn",
160
+ "@stylistic/space-unary-ops": "warn",
161
+ "@typescript-eslint/dot-notation": [ "warn", { "allowIndexSignaturePropertyAccess": true } ],
162
+ "@typescript-eslint/no-this-alias": "warn",
163
+ "camelcase": "warn",
164
+ "curly": [ "warn", "all" ],
165
+ "dot-notation": "off",
166
+ "eqeqeq": "warn",
167
+ "logical-assignment-operators": [ "warn", "always", { "enforceForIfStatements": true } ],
168
+ "no-await-in-loop": "warn",
169
+ "no-console": "warn",
170
+ "no-restricted-syntax": [ "warn", "TemplateLiteral" ],
171
+ "prefer-arrow-callback": "warn",
172
+ "prefer-const": "warn",
173
+ "quotes": [ "warn", "double", { "allowTemplateLiterals": false, "avoidEscape": false } ],
174
+ "sort-imports": "warn",
175
+ "sort-keys": "warn",
176
+ "sort-vars": "warn"
177
+ };
178
+
179
+ /**
180
+ * Browser-environment globals typically present in Homebridge webUI files - `window`, `document`, `fetch`, the Homebridge configuration API entry
181
+ * point, and a few timer functions. Each is declared as `"readonly"` so `no-undef` accepts references without permitting reassignment.
182
+ *
183
+ * Pass into the `languageOptions.globals` slot of a flat config block scoped to UI files.
184
+ */
185
+ const globalsUi = Object.fromEntries([ "clearTimeout", "console", "container", "document", "fetch", "getComputedStyle", "homebridge", "setTimeout", "window" ]
186
+ .map((key) => [ key, "readonly" ]));
187
+
188
+ /**
189
+ * Build a ready-to-use ESLint flat config array. Consumers call this function and export default the result.
190
+ *
191
+ * Every option is optional and defaults to an empty array, mirrored by the destructuring defaults in the signature below. The JSDoc marks them optional with the
192
+ * bracket form so the emitted declaration types each property as optional, matching how consumers actually call this - passing only the subset they need.
193
+ *
194
+ * @param {object} [options] - Configuration options.
195
+ * @param {string[]} [options.ts] - Glob patterns for TypeScript files (strict + stylistic type-checked rules).
196
+ * @param {string[]} [options.js] - Glob patterns for JavaScript files (disable-type-checked rules).
197
+ * @param {string[]} [options.ui] - Glob patterns for browser UI files (adds browser globals).
198
+ * @param {string[]} [options.allowDefaultProject] - Globs passed to parserOptions.projectService.allowDefaultProject.
199
+ * @param {string[]} [options.ignores] - Global ignore patterns (in addition to the hardcoded "dist" ignore on the common block).
200
+ * @param {object[]} [options.extraConfigs] - Additional flat config objects appended to the output.
201
+ * @returns {object[]} A flat config array suitable for `export default` in eslint.config.mjs. Block ordering is significant - do not reorder.
202
+ */
203
+ function config({
204
+ allowDefaultProject = [],
205
+ extraConfigs = [],
206
+ ignores = [],
207
+ js = [],
208
+ ts: tsFiles = [],
209
+ ui = []
210
+ } = {}) {
211
+
212
+ const allFiles = [ ...tsFiles, ...js, ...ui ];
213
+ const configs = [];
214
+
215
+ // Global ignores block.
216
+ if(ignores.length > 0) {
217
+
218
+ configs.push({ ignores });
219
+ }
220
+
221
+ // Core ESLint recommended rules. This block must precede the TS/JS blocks so that their rule overrides (e.g., no-unused-vars: "off") take priority.
222
+ if(allFiles.length > 0) {
223
+
224
+ configs.push(eslintJs.configs.recommended);
225
+ }
226
+
227
+ // TypeScript-specific rules.
228
+ if(tsFiles.length > 0) {
229
+
230
+ configs.push({
231
+
232
+ files: tsFiles,
233
+ rules: { ...tsRules }
234
+ });
235
+ }
236
+
237
+ // JavaScript-specific rules.
238
+ if(js.length > 0) {
239
+
240
+ configs.push({
241
+
242
+ files: js,
243
+ rules: { ...jsRules }
244
+ });
245
+ }
246
+
247
+ // Common rules applied to all linted files.
248
+ if(allFiles.length > 0) {
249
+
250
+ configs.push({
251
+
252
+ files: allFiles,
253
+
254
+ ignores: ["dist"],
255
+
256
+ languageOptions: {
257
+
258
+ ecmaVersion: "latest",
259
+ parser: ts.parser,
260
+ parserOptions: {
261
+
262
+ ecmaVersion: "latest",
263
+
264
+ projectService: {
265
+
266
+ allowDefaultProject,
267
+ defaultProject: "./tsconfig.json"
268
+ }
269
+ },
270
+
271
+ sourceType: "module"
272
+ },
273
+
274
+ linterOptions: {
275
+
276
+ reportUnusedDisableDirectives: "error"
277
+ },
278
+
279
+ plugins: { ...plugins },
280
+
281
+ rules: { ...commonRules }
282
+ });
283
+ }
284
+
285
+ // UI globals block.
286
+ if(ui.length > 0) {
287
+
288
+ configs.push({
289
+
290
+ files: ui,
291
+
292
+ languageOptions: {
293
+
294
+ globals: { ...globalsUi }
295
+ }
296
+ });
297
+ }
298
+
299
+ // Escape hatch for project-specific config blocks.
300
+ configs.push(...extraConfigs);
301
+
302
+ return configs;
303
+ }
304
+
305
+ export default config;
306
+
307
+ // Named exports for advanced customization or gradual migration.
308
+ export { commonRules, config, globalsUi, jsRules, plugins, tsRules };
@@ -0,0 +1,7 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * index.mjs: Public surface of the bundled `@hjdhjd` ESLint plugin. Re-exports the plugin object and the opinionated config builder.
4
+ */
5
+ export { default as plugin } from "./plugin.mjs";
6
+ export { commonRules, config, globalsUi, jsRules, plugins, tsRules } from "./config.mjs";
7
+ export { default } from "./config.mjs";