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
@@ -0,0 +1,354 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * split-type-imports.mjs: Require type imports and re-exports to live in declaration-level `import type` / `export type` statements rather than as
4
+ * specifier-level qualifiers (`import { type Foo, bar } from "..."`).
5
+ */
6
+
7
+ // Test whether a named specifier carries an inline `type` qualifier. ImportSpecifier exposes the qualifier on `importKind`; ExportSpecifier exposes it on
8
+ // `exportKind`. Either being `"type"` indicates the qualifier is present.
9
+ function isInlineTypeSpecifier(specifier) {
10
+
11
+ return (specifier.importKind ?? specifier.exportKind) === "type";
12
+ }
13
+
14
+ // Slide an exclusive end offset forward past a trailing semicolon. The typescript-eslint parser's declaration range[1] usually stops at the source
15
+ // string's closing quote (or the import-attributes clause), leaving the terminating `;` as a separate punctuator token. Sliding forward when the
16
+ // character at the current end is `;` makes the replacement range cover the original semicolon so the rewritten text supplies its own without doubling.
17
+ function includeTrailingSemicolon(sourceCode, end) {
18
+
19
+ return (sourceCode.text[end] === ";") ? (end + 1) : end;
20
+ }
21
+
22
+ // Slide an exclusive end offset back past a trailing semicolon. The mirror of `includeTrailingSemicolon` for the case where range[1] already covers the
23
+ // terminator: when the character just before `end` is `;`, return `end - 1` so the resulting offset stops at the last token of the declaration's body.
24
+ // The two helpers together let downstream code work uniformly against either parser configuration.
25
+ function excludeTrailingSemicolon(sourceCode, end) {
26
+
27
+ return (sourceCode.text[end - 1] === ";") ? (end - 1) : end;
28
+ }
29
+
30
+ // Reconstruct a named specifier's source text without its leading `type` qualifier. For inline-marked specifiers, the first token is the keyword `type`,
31
+ // so we slice from immediately after that keyword's range through the specifier's end and trim. For unmarked specifiers, we take the specifier text
32
+ // verbatim. Range-based slicing carries alias clauses (`as Local`) and any intra-specifier comments along with the name without name-based reconstruction.
33
+ function specifierTextStripped(sourceCode, specifier) {
34
+
35
+ if(isInlineTypeSpecifier(specifier)) {
36
+
37
+ const firstToken = sourceCode.getFirstToken(specifier);
38
+
39
+ return sourceCode.text.slice(firstToken.range[1], specifier.range[1]).trim();
40
+ }
41
+
42
+ return sourceCode.text.slice(specifier.range[0], specifier.range[1]).trim();
43
+ }
44
+
45
+ // Compute the [start, end] offsets of the declaration's post-source clause. The clause covers everything between the source string's closing quote and
46
+ // the terminating semicolon (or the declaration's end when the parser already excludes the semicolon). For most declarations the slice is empty; for
47
+ // declarations carrying `with { type: "json" }` import attributes (or the legacy `assert { ... }` form) it spans the attribute clause. This is the single
48
+ // source of truth for what counts as "the tail" - both the text-slicing path in `declarationTail` and the comment-membership check in
49
+ // `isCommentPreservable` derive their bounds from this helper so they cannot drift apart.
50
+ function declarationTailRange(sourceCode, node) {
51
+
52
+ return [ node.source.range[1], excludeTrailingSemicolon(sourceCode, node.range[1]) ];
53
+ }
54
+
55
+ // Capture the trailing clause text of a declaration after the source string. Empty for most declarations; preserves `with { type: "json" }` import
56
+ // attributes (and the legacy `assert { ... }` form) when present.
57
+ function declarationTail(sourceCode, node) {
58
+
59
+ const [ start, end ] = declarationTailRange(sourceCode, node);
60
+
61
+ return sourceCode.text.slice(start, end).trim();
62
+ }
63
+
64
+ // Compute the leading whitespace of a declaration's line. Used to indent the second emitted declaration so it lines up under the first. We read the line
65
+ // directly rather than emitting a fixed number of spaces so the indent honors tabs or mixed whitespace if either is ever in use.
66
+ function declarationIndent(sourceCode, node) {
67
+
68
+ const line = sourceCode.lines[node.loc.start.line - 1];
69
+
70
+ return line.slice(0, node.loc.start.column);
71
+ }
72
+
73
+ // Derive the kind-discriminant strings for a declaration in one place. The rule branches on whether the node is an `ImportDeclaration` or an
74
+ // `ExportNamedDeclaration` to pick the message wording, the declaration head keyword, and the kind keyword used by the value-side renderer; centralizing
75
+ // the discriminant derivations keeps them from drifting and gives every downstream consumer one object to read from instead of separate ternaries to evaluate.
76
+ function declarationFlavor(node) {
77
+
78
+ const isImport = node.type === "ImportDeclaration";
79
+
80
+ return {
81
+
82
+ head: isImport ? "import type" : "export type",
83
+ kind: isImport ? "import" : "export",
84
+ kindWord: isImport ? "Imports" : "Re-exports"
85
+ };
86
+ }
87
+
88
+ // Classify a declaration's specifiers into the buckets the rule cares about: inline-`type`-marked named specifiers, unmarked named (value) specifiers,
89
+ // and a single default specifier (imports only). Pure - takes the raw specifiers array and returns a structured object, with no dependency on `sourceCode`
90
+ // or `context`. Used by the rule's per-declaration check to drive the decision tree (skip vs split vs collapse) and by the render helpers downstream.
91
+ // Namespace specifiers (`* as ns`) fall through the switch unclassified: per the ECMAScript `ImportClause` grammar they never appear alongside named
92
+ // specifiers, so a declaration containing one yields empty `typeSpecs` and the rule's `typeSpecs.length === 0` short-circuit dismisses it.
93
+ function classifySpecifiers(specifiers) {
94
+
95
+ const typeSpecs = [];
96
+ const valueSpecs = [];
97
+ let defaultSpec = null;
98
+
99
+ for(const specifier of specifiers) {
100
+
101
+ switch(specifier.type) {
102
+
103
+ case "ImportSpecifier":
104
+ case "ExportSpecifier": {
105
+
106
+ if(isInlineTypeSpecifier(specifier)) {
107
+
108
+ typeSpecs.push(specifier);
109
+ } else {
110
+
111
+ valueSpecs.push(specifier);
112
+ }
113
+
114
+ break;
115
+ }
116
+
117
+ case "ImportDefaultSpecifier": {
118
+
119
+ defaultSpec = specifier;
120
+
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ return { defaultSpec, typeSpecs, valueSpecs };
127
+ }
128
+
129
+ // Test whether a comment that sits inside the declaration's range falls inside one of the regions the autofix preserves: any specifier's own range
130
+ // (carried along by range-based slicing) and the post-source tail range (carried verbatim into both emitted declarations).
131
+ function isCommentPreservable(sourceCode, comment, node) {
132
+
133
+ for(const specifier of node.specifiers) {
134
+
135
+ if((specifier.type !== "ImportSpecifier") && (specifier.type !== "ExportSpecifier")) {
136
+
137
+ continue;
138
+ }
139
+
140
+ if((comment.range[0] >= specifier.range[0]) && (comment.range[1] <= specifier.range[1])) {
141
+
142
+ return true;
143
+ }
144
+ }
145
+
146
+ const [ tailStart, tailEnd ] = declarationTailRange(sourceCode, node);
147
+
148
+ return (comment.range[0] >= tailStart) && (comment.range[1] <= tailEnd);
149
+ }
150
+
151
+ // Detect whether any comment in or visually adjacent to the declaration lives outside the regions the autofix actually preserves. Unpreservable comments
152
+ // include those between `import` and the brace, between commas in the brace list, between `}` and `from`, between `from` and the source string, and
153
+ // same-line leading or trailing comments that visually attach to the declaration's lines but fall outside the replacement range. The presence of any such
154
+ // comment disqualifies the declaration from autofix, and the rule falls back to report-only so the author can rewrite the declaration deliberately.
155
+ function hasUnpreservableCommentInDeclaration(sourceCode, node) {
156
+
157
+ for(const comment of sourceCode.getCommentsInside(node)) {
158
+
159
+ if(isCommentPreservable(sourceCode, comment, node)) {
160
+
161
+ continue;
162
+ }
163
+
164
+ return true;
165
+ }
166
+
167
+ for(const comment of sourceCode.getCommentsBefore(node)) {
168
+
169
+ if(comment.loc.end.line === node.loc.start.line) {
170
+
171
+ return true;
172
+ }
173
+ }
174
+
175
+ for(const comment of sourceCode.getCommentsAfter(node)) {
176
+
177
+ if(comment.loc.start.line === node.loc.end.line) {
178
+
179
+ return true;
180
+ }
181
+ }
182
+
183
+ return false;
184
+ }
185
+
186
+ // Render the type-only declaration line. The flavor's `head` field selects between `import type` / `export type`; the rest is uniform.
187
+ function renderTypeDeclaration({ flavor, sourceText, tailSuffix, typeText }) {
188
+
189
+ return flavor.head + " { " + typeText + " } from " + sourceText + tailSuffix + ";";
190
+ }
191
+
192
+ // Render the value-side declaration line. For re-exports the form is always `export { ... } from "x"` since re-exports cannot carry a default. For
193
+ // imports, the form depends on whether a default specifier survives the split and whether any named value specifiers remain: a default with no named
194
+ // values collapses the brace clause entirely (`import D from "x"`), a default with named values keeps both (`import D, { ... } from "x"`), and no
195
+ // default emits the bare brace form (`import { ... } from "x"`).
196
+ function renderValueDeclaration({ defaultSpec, flavor, sourceText, tailSuffix, valueText }) {
197
+
198
+ if(flavor.kind === "export") {
199
+
200
+ return "export { " + valueText + " } from " + sourceText + tailSuffix + ";";
201
+ }
202
+
203
+ if((defaultSpec !== null) && !valueText) {
204
+
205
+ return "import " + defaultSpec.local.name + " from " + sourceText + tailSuffix + ";";
206
+ }
207
+
208
+ if(defaultSpec !== null) {
209
+
210
+ return "import " + defaultSpec.local.name + ", { " + valueText + " } from " + sourceText + tailSuffix + ";";
211
+ }
212
+
213
+ return "import { " + valueText + " } from " + sourceText + tailSuffix + ";";
214
+ }
215
+
216
+ // Render the canonical replacement text for a declaration that contains inline `type` specifiers. When the declaration's remaining content (after lifting
217
+ // out the inline types) is empty - no value specifiers and no default - the result is a single top-level `import type` / `export type` declaration. When
218
+ // content remains, the result is two declarations: a type-only declaration first, then a regular declaration carrying the values and any default.
219
+ // Invariant at the call site: `typeSpecs.length > 0` (the `checkDeclaration` short-circuit). By the ECMAScript `ImportClause` grammar this implies no
220
+ // namespace specifier sits alongside the named specifiers; by the parser's specifier-kind contract it also implies the declaration is not itself top-level
221
+ // type-only (such a declaration parses with `importKind === "value"` on its specifiers, so `typeSpecs` would have stayed empty).
222
+ function renderSplit({ defaultSpec, flavor, node, sourceCode, typeSpecs, valueSpecs }) {
223
+
224
+ const sourceText = sourceCode.getText(node.source);
225
+ const tail = declarationTail(sourceCode, node);
226
+ const tailSuffix = tail ? (" " + tail) : "";
227
+ const typeText = typeSpecs.map((s) => specifierTextStripped(sourceCode, s)).join(", ");
228
+ const typeDecl = renderTypeDeclaration({ flavor, sourceText, tailSuffix, typeText });
229
+
230
+ if((valueSpecs.length === 0) && (defaultSpec === null)) {
231
+
232
+ return typeDecl;
233
+ }
234
+
235
+ const indent = declarationIndent(sourceCode, node);
236
+ const valueText = valueSpecs.map((s) => specifierTextStripped(sourceCode, s)).join(", ");
237
+ const valueDecl = renderValueDeclaration({ defaultSpec, flavor, sourceText, tailSuffix, valueText });
238
+
239
+ return typeDecl + "\n" + indent + valueDecl;
240
+ }
241
+
242
+ // Require type imports and re-exports to live at the declaration level (`import type` / `export type`) rather than as inline specifier qualifiers
243
+ // (`import { type Foo, bar } from "..."`).
244
+ //
245
+ // Policy rationale (single source of truth - if this comment moves, update the README and the plugin index too):
246
+ //
247
+ // The TypeScript grammar draws a real line between a declaration that exists only at compile time (`import type { ... }` is fully erased) and a
248
+ // declaration that produces a runtime import. The inline `type` qualifier is a per-specifier accommodation introduced in TS 4.5 so that non-tsc
249
+ // transpilers (Babel, esbuild, swc) could elide individual type-only specifiers without whole-program type analysis. It works, but it expresses type-ness
250
+ // at the specifier level - smuggling an erased token into a runtime declaration and asking the reader to classify each specifier in the brace list.
251
+ //
252
+ // Choosing to express type-ness at the declaration level wherever possible aligns the file's shape with the language's own categorization. A reader
253
+ // scanning the top of a file sees two flat tiers: lines that survive to runtime (`import { ... }`) and lines that do not (`import type { ... }`). Brace
254
+ // contents stay single-kind. Refactors that flip a name between type-only and runtime use move a specifier between two stable lines instead of reshaping
255
+ // a mixed declaration. And `^import type` becomes a complete grep for compile-time-only dependencies, which is invisible under the inline form.
256
+ //
257
+ // This rule pairs with `@typescript-eslint/consistent-type-imports` configured with `prefer: "type-imports"` and `fixStyle: "separate-type-imports"`.
258
+ // That upstream rule owns the question "is this type-only specifier marked at all?"; this rule owns "is the marking in the canonical declaration-level
259
+ // form?". The two concerns are crisp and non-overlapping; together they fully express the split-form policy.
260
+ //
261
+ // Cases covered:
262
+ // * `import { type A, foo } from "x"` flagged; autofix splits into `import type { A } from "x";\nimport { foo } from "x";`.
263
+ // * `import { type A, type B } from "x"` flagged; autofix collapses to `import type { A, B } from "x";` (no value side).
264
+ // * `import { type A as Aa, foo } from "x"` flagged; autofix preserves aliases via range-based slicing.
265
+ // * `import D, { type A, foo } from "x"` flagged; autofix splits with the default riding the value declaration.
266
+ // * `import D, { type A } from "x"` flagged; autofix splits and omits the empty value brace (`import D from "x";`).
267
+ // * `export { type A, foo } from "x"` flagged; same split for re-exports.
268
+ // * Import attributes (`with { type: "json" }`) carried verbatim into both emitted declarations.
269
+ // * `import type { A } from "x"` canonical; ignored.
270
+ // * `import { foo } from "x"` no type specifier; ignored (upstream owns the unmarked-type case).
271
+ //
272
+ // Comment safety: the autofix is suppressed and the rule falls back to report-only when a comment in the declaration falls outside the two preservable
273
+ // regions (any specifier's own range, and the post-source tail slice). Comments between brace tokens, between `from` and the source string, or attached
274
+ // to the same line as the declaration are unpreservable and disqualify the autofix.
275
+ const ruleSplitTypeImports = {
276
+
277
+ create(context) {
278
+
279
+ const sourceCode = context.sourceCode;
280
+
281
+ function checkDeclaration(node) {
282
+
283
+ const { defaultSpec, typeSpecs, valueSpecs } = classifySpecifiers(node.specifiers);
284
+
285
+ if(typeSpecs.length === 0) {
286
+
287
+ return;
288
+ }
289
+
290
+ const flavor = declarationFlavor(node);
291
+ const sourceText = sourceCode.getText(node.source);
292
+ const baseMessage = flavor.kindWord + " from " + sourceText + ": type specifiers must live in a separate `" + flavor.head +
293
+ "` declaration, not as inline `type` qualifiers.";
294
+ const loc = { end: typeSpecs.at(-1).loc.end, start: typeSpecs[0].loc.start };
295
+
296
+ if(hasUnpreservableCommentInDeclaration(sourceCode, node)) {
297
+
298
+ context.report({
299
+
300
+ loc,
301
+ message: baseMessage +
302
+ " Autofix suppressed because a comment in the declaration cannot be safely preserved by the rewrite; rewrite the declaration manually.",
303
+ node
304
+ });
305
+
306
+ return;
307
+ }
308
+
309
+ const replacement = renderSplit({ defaultSpec, flavor, node, sourceCode, typeSpecs, valueSpecs });
310
+ const range = [ node.range[0], includeTrailingSemicolon(sourceCode, node.range[1]) ];
311
+
312
+ context.report({
313
+
314
+ fix(fixer) {
315
+
316
+ return fixer.replaceTextRange(range, replacement);
317
+ },
318
+ loc,
319
+ message: baseMessage,
320
+ node
321
+ });
322
+ }
323
+
324
+ return {
325
+
326
+ ExportNamedDeclaration(node) {
327
+
328
+ if(node.source !== null) {
329
+
330
+ checkDeclaration(node);
331
+ }
332
+ },
333
+
334
+ ImportDeclaration(node) {
335
+
336
+ checkDeclaration(node);
337
+ }
338
+ };
339
+ },
340
+ meta: {
341
+
342
+ docs: {
343
+
344
+ description: "require type imports and re-exports to live in declaration-level `import type` / `export type` statements rather than as inline " +
345
+ "specifier-level `type` qualifiers",
346
+ recommended: false
347
+ },
348
+ fixable: "code",
349
+ schema: [],
350
+ type: "suggestion"
351
+ }
352
+ };
353
+
354
+ export default ruleSplitTypeImports;
@@ -0,0 +1,268 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * split-type-imports.test.mjs: Unit tests for the split-type-imports rule. Covers the single split and collapse cases, default-specifier handling,
4
+ * re-exports, alias and import-attribute preservation, comment-safety suppression across every unpreservable region, and the negative passthrough cases
5
+ * that prove canonical declarations and namespace / default-only / unmarked imports are correctly skipped.
6
+ */
7
+ import { RuleTester } from "../test-setup.mjs";
8
+ import rule from "./split-type-imports.mjs";
9
+ import ts from "typescript-eslint";
10
+
11
+ // RuleTester wired to the typescript-eslint parser. The rule needs the typescript-eslint AST shape - inline `type` qualifiers on specifiers manifest as
12
+ // `importKind` / `exportKind` fields that espree (the default parser) does not produce.
13
+ const ruleTester = new RuleTester({
14
+
15
+ languageOptions: {
16
+
17
+ parser: ts.parser
18
+ }
19
+ });
20
+
21
+ ruleTester.run("split-type-imports", rule, {
22
+
23
+ invalid: [
24
+
25
+ // Single inline type with a value specifier. Canonical form lifts the type to its own `import type` declaration and leaves the value declaration
26
+ // behind. Specifier text round-trips exactly via range-based slicing.
27
+ {
28
+
29
+ code: "import { type A, foo } from \"x\";",
30
+ errors: [{ message: /Imports from "x": type specifiers must live in a separate `import type` declaration/ }],
31
+ output: "import type { A } from \"x\";\nimport { foo } from \"x\";"
32
+ },
33
+
34
+ // Multiple inline types with a value specifier. The type declaration carries every inline-marked specifier in source order; the value declaration
35
+ // retains the remaining value specifier(s) in source order. Member sort is owned by `sort-imports` and is not this rule's concern.
36
+ {
37
+
38
+ code: "import { type A, type B, foo } from \"x\";",
39
+ errors: 1,
40
+ output: "import type { A, B } from \"x\";\nimport { foo } from \"x\";"
41
+ },
42
+
43
+ // Multiple types and multiple values. Splits cleanly.
44
+ {
45
+
46
+ code: "import { type A, type B, foo, bar } from \"x\";",
47
+ errors: 1,
48
+ output: "import type { A, B } from \"x\";\nimport { foo, bar } from \"x\";"
49
+ },
50
+
51
+ // Collapse case: every named specifier carries the inline qualifier, no values remain. Canonical form is the single top-level type-only declaration;
52
+ // the rule emits no second declaration.
53
+ {
54
+
55
+ code: "import { type A } from \"x\";",
56
+ errors: 1,
57
+ output: "import type { A } from \"x\";"
58
+ },
59
+
60
+ // Collapse case with multiple inline types.
61
+ {
62
+
63
+ code: "import { type A, type B, type C } from \"x\";",
64
+ errors: 1,
65
+ output: "import type { A, B, C } from \"x\";"
66
+ },
67
+
68
+ // Default specifier riding alongside inline types and values. The default joins the value declaration (`import D, { ... } from "x"`) since its
69
+ // runtime semantics are preserved by staying on the value side.
70
+ {
71
+
72
+ code: "import D, { type A, foo } from \"x\";",
73
+ errors: 1,
74
+ output: "import type { A } from \"x\";\nimport D, { foo } from \"x\";"
75
+ },
76
+
77
+ // Default specifier with only inline types and no value specifiers. The value declaration collapses its empty brace clause to the bare default form
78
+ // `import D from "x"`, since an empty `{}` is a syntactic but visually ugly artifact.
79
+ {
80
+
81
+ code: "import D, { type A } from \"x\";",
82
+ errors: 1,
83
+ output: "import type { A } from \"x\";\nimport D from \"x\";"
84
+ },
85
+
86
+ // Default specifier with multiple inline types and no values - same collapse of the empty brace clause.
87
+ {
88
+
89
+ code: "import D, { type A, type B } from \"x\";",
90
+ errors: 1,
91
+ output: "import type { A, B } from \"x\";\nimport D from \"x\";"
92
+ },
93
+
94
+ // Alias preservation on inline type specifiers via range-based slicing. `type A as Aa` round-trips intact, including whitespace inside the alias.
95
+ {
96
+
97
+ code: "import { type A as Aa, foo } from \"x\";",
98
+ errors: 1,
99
+ output: "import type { A as Aa } from \"x\";\nimport { foo } from \"x\";"
100
+ },
101
+
102
+ // Alias preservation on value specifiers too. Range-based slicing carries `foo as bar` through verbatim.
103
+ {
104
+
105
+ code: "import { type A, foo as bar } from \"x\";",
106
+ errors: 1,
107
+ output: "import type { A } from \"x\";\nimport { foo as bar } from \"x\";"
108
+ },
109
+
110
+ // Import attributes (the `with { type: "json" }` clause) are carried verbatim into both emitted declarations so the runtime loader honors them on the
111
+ // value side and the compiler honors them on the type side.
112
+ {
113
+
114
+ code: "import { type A, foo } from \"x\" with { type: \"json\" };",
115
+ errors: 1,
116
+ output: "import type { A } from \"x\" with { type: \"json\" };\nimport { foo } from \"x\" with { type: \"json\" };"
117
+ },
118
+
119
+ // Intra-specifier block comments are preserved by range-based slicing. The comment lives inside the specifier's range and rides along when its text
120
+ // is sliced into the type-declaration output.
121
+ {
122
+
123
+ code: "import { type /* note */ A, foo } from \"x\";",
124
+ errors: 1,
125
+ output: "import type { /* note */ A } from \"x\";\nimport { foo } from \"x\";"
126
+ },
127
+
128
+ // Re-export form: inline type plus value specifier. Same split shape with `export type` / `export` heads.
129
+ {
130
+
131
+ code: "export { type A, foo } from \"x\";",
132
+ errors: [{ message: /Re-exports from "x": type specifiers must live in a separate `export type` declaration/ }],
133
+ output: "export type { A } from \"x\";\nexport { foo } from \"x\";"
134
+ },
135
+
136
+ // Re-export form: collapse case when every named specifier is type-only.
137
+ {
138
+
139
+ code: "export { type A, type B } from \"x\";",
140
+ errors: 1,
141
+ output: "export type { A, B } from \"x\";"
142
+ },
143
+
144
+ // Inline source order preserved: the rule does not sort. Z, Y, X stays Z, Y, X. `sort-imports` handles within-brace alphabetization on a subsequent
145
+ // autofix pass.
146
+ {
147
+
148
+ code: "import { type Z, type Y, type X, foo } from \"x\";",
149
+ errors: 1,
150
+ output: "import type { Z, Y, X } from \"x\";\nimport { foo } from \"x\";"
151
+ },
152
+
153
+ // Inter-specifier block comment: unpreservable. The comment's destination after the split (which side of the split, between which two specifiers) is
154
+ // genuinely ambiguous, and silently picking one is worse than leaving the rewrite to a human. Report the violation, suppress the autofix.
155
+ {
156
+
157
+ code: "import { type A, /* between */ foo } from \"x\";",
158
+ errors: [{ message: /Autofix suppressed because a comment in the declaration cannot be safely preserved/ }],
159
+ output: null
160
+ },
161
+
162
+ // Inter-specifier line comment: same suppression. The helper considers any comment kind, not just block comments.
163
+ {
164
+
165
+ code: "import { type A,\n // pinned for now\n foo } from \"x\";",
166
+ errors: 1,
167
+ output: null
168
+ },
169
+
170
+ // Comment between the `import` keyword and the default specifier. The autofix replaces the entire declaration and would otherwise drop this comment.
171
+ {
172
+
173
+ code: "import /* default-only */ D, { type A, foo } from \"x\";",
174
+ errors: 1,
175
+ output: null
176
+ },
177
+
178
+ // Trailing comment inside the brace list - after the last specifier, before the closing brace. Outside any specifier's range and outside the
179
+ // post-source tail slice.
180
+ {
181
+
182
+ code: "import { type A, foo /* trailing */ } from \"x\";",
183
+ errors: 1,
184
+ output: null
185
+ },
186
+
187
+ // Comment between the closing brace and the `from` keyword - same unpreservable region.
188
+ {
189
+
190
+ code: "import { type A, foo } /* trailing */ from \"x\";",
191
+ errors: 1,
192
+ output: null
193
+ },
194
+
195
+ // Comment between `from` and the source string literal - again unpreservable, since `getText(node.source)` only captures the literal itself and the
196
+ // tail slice starts at the end of that literal.
197
+ {
198
+
199
+ code: "import { type A, foo } from /* path */ \"x\";",
200
+ errors: 1,
201
+ output: null
202
+ },
203
+
204
+ // Comment inside the post-source tail slice (around an import-attributes clause) IS preserved by `declarationTail`, so the autofix runs normally
205
+ // and the comment survives in both emitted declarations.
206
+ {
207
+
208
+ code: "import { type A, foo } from \"x\" /* odd */ with { type: \"json\" };",
209
+ errors: 1,
210
+ output: "import type { A } from \"x\" /* odd */ with { type: \"json\" };\nimport { foo } from \"x\" /* odd */ with { type: \"json\" };"
211
+ },
212
+
213
+ // Same-line trailing comment after the declaration's semicolon. The comment survives the rewrite but lands visually attached to the last emitted
214
+ // declaration, even though the author wrote it for the whole import. Treated as unpreservable.
215
+ {
216
+
217
+ code: "import { type A, foo } from \"x\"; // legacy import",
218
+ errors: 1,
219
+ output: null
220
+ },
221
+
222
+ // Same-line leading block comment before the declaration. After a split the comment would label only the first emitted declaration.
223
+ {
224
+
225
+ code: "/* legacy */ import { type A, foo } from \"x\";",
226
+ errors: 1,
227
+ output: null
228
+ },
229
+
230
+ // Comments on adjacent lines (not the same line as the declaration) are not visually attached to the declaration's line and do not trigger
231
+ // suppression - autofix runs as normal and the surrounding comments survive in place because they fall outside the replacement range.
232
+ {
233
+
234
+ code: "// previous-line comment\nimport { type A, foo } from \"x\";\n// following-line comment",
235
+ errors: 1,
236
+ output: "// previous-line comment\nimport type { A } from \"x\";\nimport { foo } from \"x\";\n// following-line comment"
237
+ }
238
+ ],
239
+
240
+ valid: [
241
+
242
+ // Already top-level type-only declarations - canonical form. The rule has nothing to do.
243
+ "import type { A } from \"x\";",
244
+ "import type { A, B, C } from \"x\";",
245
+ "import type D from \"x\";",
246
+ "import type { A, B } from \"x\";\nimport { foo } from \"x\";",
247
+
248
+ // Plain value imports of every shape - no inline `type` qualifier means no policy violation. The upstream `consistent-type-imports` rule handles
249
+ // unmarked types separately; this rule's domain is only the inline-marked case.
250
+ "import { foo } from \"x\";",
251
+ "import { foo, bar } from \"x\";",
252
+ "import D from \"x\";",
253
+ "import D, { foo } from \"x\";",
254
+ "import * as ns from \"x\";",
255
+ "import \"x\";",
256
+
257
+ // Re-exports without inline `type` qualifiers.
258
+ "export { foo } from \"x\";",
259
+ "export { foo, bar } from \"x\";",
260
+ "export type { A, B } from \"x\";",
261
+
262
+ // Local re-export without a source clause - nothing for this rule to act on.
263
+ "const foo = 1; export { foo };",
264
+
265
+ // Namespace imports are never combined with named specifiers, so even an otherwise-mixed file leaves them untouched.
266
+ "import * as ns from \"x\";\nimport { foo } from \"y\";"
267
+ ]
268
+ });
@@ -0,0 +1,21 @@
1
+ /* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * test-setup.mjs: Shared RuleTester scaffold for the plugin's per-rule test suites. Wires ESLint's `RuleTester` static class to the `node:test` runner's
4
+ * `describe` / `it` hooks so each rule's `*.test.mjs` file can construct a tester without repeating the static-assignment dance. The assignments mutate
5
+ * the `RuleTester` class itself, so they run exactly once on first import regardless of how many test files reach in.
6
+ *
7
+ * Mirrors the host package's `src/testing.helpers.ts` convention - shared test infrastructure lives at the top of its module, alongside the entry-point
8
+ * files (`plugin.mjs`, `config.mjs`, `index.mjs`), with co-located test files in `rules/` importing back up via `../test-setup.mjs`.
9
+ */
10
+ import { describe, it } from "node:test";
11
+ import { RuleTester } from "eslint";
12
+
13
+ RuleTester.describe = describe;
14
+ RuleTester.it = it;
15
+
16
+ // `RuleTester.itOnly` is dispatched when a test case is declared with `{ only: true, ... }`. We map it to `it.only` (not plain `it`) so the test case
17
+ // is actually marked exclusive. Note: node:test additionally requires the `--test-only` CLI flag for `.only` markers to take effect; without it the
18
+ // markers are silently ignored and every test runs as normal. Use `npm run test:focus` (or `node --test --test-only ...`) when debugging with `only: true`.
19
+ RuleTester.itOnly = it.only;
20
+
21
+ export { RuleTester };