@nifrajs/cli 4.0.2 → 5.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 (364) hide show
  1. package/README.md +10 -2
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +9 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/assure.d.ts +12 -2
  14. package/dist/assure.d.ts.map +1 -1
  15. package/dist/assure.js +59 -8
  16. package/dist/assure.js.map +1 -1
  17. package/dist/capabilities-tool.d.ts.map +1 -1
  18. package/dist/capabilities-tool.js +36 -9
  19. package/dist/capabilities-tool.js.map +1 -1
  20. package/dist/check-diagnostics.d.ts +22 -2
  21. package/dist/check-diagnostics.d.ts.map +1 -1
  22. package/dist/check-diagnostics.js +54 -7
  23. package/dist/check-diagnostics.js.map +1 -1
  24. package/dist/check-scan.d.ts +83 -24
  25. package/dist/check-scan.d.ts.map +1 -1
  26. package/dist/check-scan.js +1516 -318
  27. package/dist/check-scan.js.map +1 -1
  28. package/dist/check.d.ts +2 -2
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +203 -70
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +9 -0
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +215 -50
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client-module.d.ts +20 -0
  37. package/dist/client-module.d.ts.map +1 -0
  38. package/dist/client-module.js +73 -0
  39. package/dist/client-module.js.map +1 -0
  40. package/dist/command-catalog.d.ts +24 -5
  41. package/dist/command-catalog.d.ts.map +1 -1
  42. package/dist/command-catalog.js +194 -45
  43. package/dist/command-catalog.js.map +1 -1
  44. package/dist/component-check.d.ts +13 -0
  45. package/dist/component-check.d.ts.map +1 -0
  46. package/dist/component-check.js +59 -0
  47. package/dist/component-check.js.map +1 -0
  48. package/dist/contract-proof.d.ts +1 -1
  49. package/dist/contract-proof.d.ts.map +1 -1
  50. package/dist/contract-proof.js +2 -24
  51. package/dist/contract-proof.js.map +1 -1
  52. package/dist/contracts.d.ts +19 -0
  53. package/dist/contracts.d.ts.map +1 -1
  54. package/dist/contracts.js +50 -3
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db-tool.d.ts.map +1 -1
  57. package/dist/db-tool.js +4 -3
  58. package/dist/db-tool.js.map +1 -1
  59. package/dist/dev-bun-config.d.ts +5 -1
  60. package/dist/dev-bun-config.d.ts.map +1 -1
  61. package/dist/dev-bun-config.js +44 -11
  62. package/dist/dev-bun-config.js.map +1 -1
  63. package/dist/dev-feed-tool.d.ts +18 -1
  64. package/dist/dev-feed-tool.d.ts.map +1 -1
  65. package/dist/dev-feed-tool.js +93 -0
  66. package/dist/dev-feed-tool.js.map +1 -1
  67. package/dist/diagnostics.d.ts +3 -0
  68. package/dist/diagnostics.d.ts.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/diff-tool.d.ts +9 -8
  71. package/dist/diff-tool.d.ts.map +1 -1
  72. package/dist/diff-tool.js +28 -24
  73. package/dist/diff-tool.js.map +1 -1
  74. package/dist/doctor.d.ts +13 -0
  75. package/dist/doctor.d.ts.map +1 -1
  76. package/dist/doctor.js +23 -15
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/env-file.d.ts +6 -0
  79. package/dist/env-file.d.ts.map +1 -1
  80. package/dist/env-file.js +6 -0
  81. package/dist/env-file.js.map +1 -1
  82. package/dist/fix-recipes.d.ts +2 -0
  83. package/dist/fix-recipes.d.ts.map +1 -1
  84. package/dist/fix-recipes.js +102 -2
  85. package/dist/fix-recipes.js.map +1 -1
  86. package/dist/hand-built-manifest.d.ts +27 -0
  87. package/dist/hand-built-manifest.d.ts.map +1 -0
  88. package/dist/hand-built-manifest.js +124 -0
  89. package/dist/hand-built-manifest.js.map +1 -0
  90. package/dist/hooks-report.d.ts +23 -0
  91. package/dist/hooks-report.d.ts.map +1 -0
  92. package/dist/hooks-report.js +104 -0
  93. package/dist/hooks-report.js.map +1 -0
  94. package/dist/init-agents.d.ts +18 -2
  95. package/dist/init-agents.d.ts.map +1 -1
  96. package/dist/init-agents.js +20 -13
  97. package/dist/init-agents.js.map +1 -1
  98. package/dist/internal/config-paths.d.ts +16 -0
  99. package/dist/internal/config-paths.d.ts.map +1 -0
  100. package/dist/internal/config-paths.js +124 -0
  101. package/dist/internal/config-paths.js.map +1 -0
  102. package/dist/internal/installed-package.d.ts +4 -0
  103. package/dist/internal/installed-package.d.ts.map +1 -0
  104. package/dist/internal/installed-package.js +17 -0
  105. package/dist/internal/installed-package.js.map +1 -0
  106. package/dist/internal/nifra-project.d.ts +41 -0
  107. package/dist/internal/nifra-project.d.ts.map +1 -0
  108. package/dist/internal/nifra-project.js +183 -0
  109. package/dist/internal/nifra-project.js.map +1 -0
  110. package/dist/internal/project-specifiers.d.ts +29 -0
  111. package/dist/internal/project-specifiers.d.ts.map +1 -0
  112. package/dist/internal/project-specifiers.js +435 -0
  113. package/dist/internal/project-specifiers.js.map +1 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -1
  115. package/dist/internal/script-kind.js +2 -1
  116. package/dist/internal/script-kind.js.map +1 -1
  117. package/dist/internal/source-facts.d.ts +8 -1
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +22 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +1 -1
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +10 -6
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +15 -6
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +87 -25
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/levels-tool.d.ts.map +1 -1
  130. package/dist/levels-tool.js +98 -23
  131. package/dist/levels-tool.js.map +1 -1
  132. package/dist/load.d.ts +33 -10
  133. package/dist/load.d.ts.map +1 -1
  134. package/dist/load.js +60 -10
  135. package/dist/load.js.map +1 -1
  136. package/dist/mcp-context.d.ts +5 -5
  137. package/dist/mcp-context.d.ts.map +1 -1
  138. package/dist/mcp-context.js +8 -23
  139. package/dist/mcp-context.js.map +1 -1
  140. package/dist/mcp-delegate.d.ts +2 -2
  141. package/dist/mcp-delegate.d.ts.map +1 -1
  142. package/dist/mcp-delegate.js +19 -7
  143. package/dist/mcp-delegate.js.map +1 -1
  144. package/dist/mcp-exec.d.ts +13 -4
  145. package/dist/mcp-exec.d.ts.map +1 -1
  146. package/dist/mcp-exec.js +56 -14
  147. package/dist/mcp-exec.js.map +1 -1
  148. package/dist/mcp-http.js +1 -1
  149. package/dist/mcp-io.d.ts +30 -0
  150. package/dist/mcp-io.d.ts.map +1 -1
  151. package/dist/mcp-io.js +63 -0
  152. package/dist/mcp-io.js.map +1 -1
  153. package/dist/mcp-isolate.d.ts.map +1 -1
  154. package/dist/mcp-isolate.js +3 -2
  155. package/dist/mcp-isolate.js.map +1 -1
  156. package/dist/mcp-path.d.ts +2 -1
  157. package/dist/mcp-path.d.ts.map +1 -1
  158. package/dist/mcp-path.js +9 -24
  159. package/dist/mcp-path.js.map +1 -1
  160. package/dist/mcp-project-child.js +1 -1
  161. package/dist/mcp-project-child.js.map +1 -1
  162. package/dist/mcp-root.d.ts.map +1 -1
  163. package/dist/mcp-root.js +50 -10
  164. package/dist/mcp-root.js.map +1 -1
  165. package/dist/mcp-ws.d.ts.map +1 -1
  166. package/dist/mcp-ws.js +3 -1
  167. package/dist/mcp-ws.js.map +1 -1
  168. package/dist/mcp.d.ts.map +1 -1
  169. package/dist/mcp.js +48 -19
  170. package/dist/mcp.js.map +1 -1
  171. package/dist/migrate-layout.d.ts +47 -1
  172. package/dist/migrate-layout.d.ts.map +1 -1
  173. package/dist/migrate-layout.js +1972 -170
  174. package/dist/migrate-layout.js.map +1 -1
  175. package/dist/output-schemas.d.ts +128 -0
  176. package/dist/output-schemas.d.ts.map +1 -0
  177. package/dist/output-schemas.js +975 -0
  178. package/dist/output-schemas.js.map +1 -0
  179. package/dist/port.d.ts.map +1 -1
  180. package/dist/port.js +3 -2
  181. package/dist/port.js.map +1 -1
  182. package/dist/project-facts.d.ts +15 -4
  183. package/dist/project-facts.d.ts.map +1 -1
  184. package/dist/project-facts.js +1 -0
  185. package/dist/project-facts.js.map +1 -1
  186. package/dist/project-path.d.ts +6 -0
  187. package/dist/project-path.d.ts.map +1 -1
  188. package/dist/project-path.js +48 -14
  189. package/dist/project-path.js.map +1 -1
  190. package/dist/recipes/5.0.0.d.ts +11 -0
  191. package/dist/recipes/5.0.0.d.ts.map +1 -0
  192. package/dist/recipes/5.0.0.js +34 -0
  193. package/dist/recipes/5.0.0.js.map +1 -0
  194. package/dist/recipes/index.d.ts +7 -0
  195. package/dist/recipes/index.d.ts.map +1 -1
  196. package/dist/recipes/index.js +2 -0
  197. package/dist/recipes/index.js.map +1 -1
  198. package/dist/release-verification.d.ts +5 -0
  199. package/dist/release-verification.d.ts.map +1 -1
  200. package/dist/release-verification.js +14 -0
  201. package/dist/release-verification.js.map +1 -1
  202. package/dist/replay.d.ts.map +1 -1
  203. package/dist/replay.js +12 -3
  204. package/dist/replay.js.map +1 -1
  205. package/dist/review.d.ts +3 -1
  206. package/dist/review.d.ts.map +1 -1
  207. package/dist/review.js +43 -7
  208. package/dist/review.js.map +1 -1
  209. package/dist/rules/codes.d.ts +12 -0
  210. package/dist/rules/codes.d.ts.map +1 -1
  211. package/dist/rules/codes.js +12 -0
  212. package/dist/rules/codes.js.map +1 -1
  213. package/dist/rules/comment-markers.d.ts +16 -7
  214. package/dist/rules/comment-markers.d.ts.map +1 -1
  215. package/dist/rules/comment-markers.js +58 -19
  216. package/dist/rules/comment-markers.js.map +1 -1
  217. package/dist/rules/contract-visibility.d.ts +3 -0
  218. package/dist/rules/contract-visibility.d.ts.map +1 -0
  219. package/dist/rules/contract-visibility.js +337 -0
  220. package/dist/rules/contract-visibility.js.map +1 -0
  221. package/dist/rules/data-guard.d.ts +13 -0
  222. package/dist/rules/data-guard.d.ts.map +1 -1
  223. package/dist/rules/data-guard.js +356 -37
  224. package/dist/rules/data-guard.js.map +1 -1
  225. package/dist/rules/layout.d.ts +4 -0
  226. package/dist/rules/layout.d.ts.map +1 -0
  227. package/dist/rules/layout.js +19 -0
  228. package/dist/rules/layout.js.map +1 -0
  229. package/dist/rules/legacy.d.ts.map +1 -1
  230. package/dist/rules/legacy.js +178 -44
  231. package/dist/rules/legacy.js.map +1 -1
  232. package/dist/rules/pages.d.ts +50 -0
  233. package/dist/rules/pages.d.ts.map +1 -1
  234. package/dist/rules/pages.js +436 -4
  235. package/dist/rules/pages.js.map +1 -1
  236. package/dist/rules/routes.d.ts +6 -0
  237. package/dist/rules/routes.d.ts.map +1 -1
  238. package/dist/rules/routes.js +111 -16
  239. package/dist/rules/routes.js.map +1 -1
  240. package/dist/rules/secrets.js +2 -2
  241. package/dist/rules/secrets.js.map +1 -1
  242. package/dist/rules/security.d.ts +13 -1
  243. package/dist/rules/security.d.ts.map +1 -1
  244. package/dist/rules/security.js +313 -69
  245. package/dist/rules/security.js.map +1 -1
  246. package/dist/rules/zones.d.ts +13 -0
  247. package/dist/rules/zones.d.ts.map +1 -1
  248. package/dist/rules/zones.js +218 -44
  249. package/dist/rules/zones.js.map +1 -1
  250. package/dist/scaffold.d.ts.map +1 -1
  251. package/dist/scaffold.js +6 -4
  252. package/dist/scaffold.js.map +1 -1
  253. package/dist/sdk.d.ts.map +1 -1
  254. package/dist/sdk.js +14 -5
  255. package/dist/sdk.js.map +1 -1
  256. package/dist/stylex-migrate.d.ts.map +1 -1
  257. package/dist/stylex-migrate.js +290 -24
  258. package/dist/stylex-migrate.js.map +1 -1
  259. package/dist/test-tool.d.ts.map +1 -1
  260. package/dist/test-tool.js +6 -3
  261. package/dist/test-tool.js.map +1 -1
  262. package/dist/typecheck-coverage.d.ts +20 -0
  263. package/dist/typecheck-coverage.d.ts.map +1 -0
  264. package/dist/typecheck-coverage.js +129 -0
  265. package/dist/typecheck-coverage.js.map +1 -0
  266. package/dist/upgrade.d.ts +84 -0
  267. package/dist/upgrade.d.ts.map +1 -1
  268. package/dist/upgrade.js +498 -78
  269. package/dist/upgrade.js.map +1 -1
  270. package/dist/verification-plan.d.ts.map +1 -1
  271. package/dist/verification-plan.js +16 -2
  272. package/dist/verification-plan.js.map +1 -1
  273. package/dist/version-drift.d.ts +22 -0
  274. package/dist/version-drift.d.ts.map +1 -0
  275. package/dist/version-drift.js +94 -0
  276. package/dist/version-drift.js.map +1 -0
  277. package/dist/web-app-options.d.ts +1 -1
  278. package/dist/web-app-options.d.ts.map +1 -1
  279. package/dist/web-app-options.js +1 -1
  280. package/dist/web-app-options.js.map +1 -1
  281. package/dist/work-graph.d.ts.map +1 -1
  282. package/dist/work-graph.js +20 -8
  283. package/dist/work-graph.js.map +1 -1
  284. package/docs/examples.json +55 -7
  285. package/docs/llms-full.txt +2043 -222
  286. package/docs/types.json +1573 -246
  287. package/package.json +11 -12
  288. package/src/agent-files.ts +203 -0
  289. package/src/app-files.ts +34 -4
  290. package/src/assure-hydration.ts +18 -2
  291. package/src/assure.ts +88 -10
  292. package/src/capabilities-tool.ts +47 -9
  293. package/src/check-diagnostics.ts +75 -9
  294. package/src/check-scan.ts +1816 -377
  295. package/src/check.ts +258 -81
  296. package/src/cli.ts +242 -37
  297. package/src/client-module.ts +97 -0
  298. package/src/command-catalog.ts +237 -52
  299. package/src/component-check.ts +79 -0
  300. package/src/contract-proof.ts +3 -25
  301. package/src/contracts.ts +60 -3
  302. package/src/db-tool.ts +5 -2
  303. package/src/dev-bun-config.ts +51 -10
  304. package/src/dev-feed-tool.ts +112 -1
  305. package/src/diagnostics.ts +3 -0
  306. package/src/diff-tool.ts +33 -26
  307. package/src/doctor.ts +43 -21
  308. package/src/env-file.ts +7 -0
  309. package/src/fix-recipes.ts +116 -2
  310. package/src/hand-built-manifest.ts +144 -0
  311. package/src/hooks-report.ts +135 -0
  312. package/src/init-agents.ts +21 -14
  313. package/src/internal/config-paths.ts +154 -0
  314. package/src/internal/installed-package.ts +15 -0
  315. package/src/internal/nifra-project.ts +191 -0
  316. package/src/internal/project-specifiers.ts +477 -0
  317. package/src/internal/script-kind.ts +2 -1
  318. package/src/internal/source-facts.ts +48 -5
  319. package/src/internal/typescript-import.ts +15 -6
  320. package/src/introspect.ts +104 -28
  321. package/src/levels-tool.ts +124 -29
  322. package/src/load.ts +107 -17
  323. package/src/mcp-context.ts +12 -25
  324. package/src/mcp-delegate.ts +22 -10
  325. package/src/mcp-exec.ts +68 -18
  326. package/src/mcp-http.ts +1 -1
  327. package/src/mcp-io.ts +74 -0
  328. package/src/mcp-isolate.ts +9 -2
  329. package/src/mcp-path.ts +8 -22
  330. package/src/mcp-project-child.ts +1 -1
  331. package/src/mcp-root.ts +58 -8
  332. package/src/mcp-ws.ts +4 -2
  333. package/src/mcp.ts +56 -22
  334. package/src/migrate-layout.ts +2257 -176
  335. package/src/output-schemas.ts +1240 -0
  336. package/src/port.ts +3 -2
  337. package/src/project-facts.ts +17 -3
  338. package/src/project-path.ts +44 -13
  339. package/src/recipes/5.0.0.ts +35 -0
  340. package/src/recipes/index.ts +10 -0
  341. package/src/release-verification.ts +20 -0
  342. package/src/replay.ts +14 -3
  343. package/src/review.ts +44 -10
  344. package/src/rules/codes.ts +12 -0
  345. package/src/rules/comment-markers.ts +57 -21
  346. package/src/rules/contract-visibility.ts +383 -0
  347. package/src/rules/data-guard.ts +391 -42
  348. package/src/rules/layout.ts +22 -0
  349. package/src/rules/legacy.ts +206 -52
  350. package/src/rules/pages.ts +481 -4
  351. package/src/rules/routes.ts +124 -16
  352. package/src/rules/secrets.ts +2 -2
  353. package/src/rules/security.ts +391 -72
  354. package/src/rules/zones.ts +232 -42
  355. package/src/scaffold.ts +6 -4
  356. package/src/sdk.ts +14 -5
  357. package/src/stylex-migrate.ts +251 -24
  358. package/src/test-tool.ts +12 -3
  359. package/src/typecheck-coverage.ts +134 -0
  360. package/src/upgrade.ts +576 -72
  361. package/src/verification-plan.ts +33 -2
  362. package/src/version-drift.ts +118 -0
  363. package/src/web-app-options.ts +2 -2
  364. package/src/work-graph.ts +17 -12
@@ -927,17 +927,144 @@ const lineOf = (source: string, offset: number): number => {
927
927
  return line
928
928
  }
929
929
 
930
- const isLikelyJsxAttribute = (source: string, offset: number): boolean => {
931
- const tagStart = source.lastIndexOf("<", offset)
932
- const tagEnd = source.lastIndexOf(">", offset)
933
- return tagStart > tagEnd && !source.slice(tagStart, offset).includes("<!--")
930
+ /** Words after which an expression - so a regex literal or a JSX element - may start. */
931
+ const EXPRESSION_KEYWORDS = new Set([
932
+ "return",
933
+ "typeof",
934
+ "instanceof",
935
+ "in",
936
+ "of",
937
+ "new",
938
+ "delete",
939
+ "void",
940
+ "throw",
941
+ "case",
942
+ "do",
943
+ "else",
944
+ "yield",
945
+ "await",
946
+ ])
947
+
948
+ const WORD_CHAR = /[\w$]/
949
+
950
+ /** `<` at `offset` opens an element unless it starts a type parameter list (`<T,>`, `<T extends U>`). */
951
+ const opensElement = (source: string, offset: number): boolean => {
952
+ const name = /^<([A-Za-z_$][\w$.:-]*)?(\s*)/.exec(source.slice(offset, offset + 256))
953
+ if (name === null) return false
954
+ if (name[1] === undefined) return source[offset + 1] === ">"
955
+ const after = source.slice(offset + name[0].length, offset + name[0].length + 8)
956
+ return !after.startsWith(",") && !(name[2] !== "" && /^extends\b/.test(after))
934
957
  }
935
958
 
936
- const classAttributes = (source: string): readonly ClassAttribute[] => {
959
+ /**
960
+ * Which offsets of `source` sit inside a JSX tag, where an attribute can be. A small lexer, not a
961
+ * parser: it steps over strings, template literals, comments and regex literals, and follows JSX by
962
+ * the usual rule that `<` where an expression may start opens an element. A `className=` anywhere
963
+ * else - HTML in a template literal, a comment, a string, JSX text - is not an attribute.
964
+ */
965
+ function jsxTagMask(source: string, jsx: boolean): Uint8Array {
966
+ const mask = new Uint8Array(source.length)
967
+ const stack: ("code" | "template" | "tag" | "children")[] = ["code"]
968
+ const length = source.length
969
+ let expression = true
970
+ let index = 0
971
+ const skipQuoted = (quote: string, multiline: boolean): void => {
972
+ index += 1
973
+ while (index < length && source[index] !== quote && (multiline || source[index] !== "\n"))
974
+ index += source[index] === "\\" && !multiline ? 2 : 1
975
+ index += 1
976
+ }
977
+ while (index < length) {
978
+ const frame = stack[stack.length - 1]
979
+ const char = source[index] ?? ""
980
+ const next = source[index + 1]
981
+ if (frame === "template") {
982
+ if (char === "\\") index += 2
983
+ else if (char === "`") {
984
+ stack.pop()
985
+ index += 1
986
+ expression = false
987
+ } else if (char === "$" && next === "{") {
988
+ stack.push("code")
989
+ index += 2
990
+ expression = true
991
+ } else index += 1
992
+ } else if (frame === "children") {
993
+ if (char === "{") {
994
+ stack.push("code")
995
+ expression = true
996
+ } else if (char === "<" && next === "/") {
997
+ const close = source.indexOf(">", index)
998
+ index = close === -1 ? length : close
999
+ stack.pop()
1000
+ expression = false
1001
+ } else if (char === "<") stack.push("tag")
1002
+ index += 1
1003
+ } else if (frame === "tag") {
1004
+ mask[index] = 1
1005
+ if (char === '"' || char === "'") skipQuoted(char, true)
1006
+ else if (char === "{") {
1007
+ stack.push("code")
1008
+ expression = true
1009
+ index += 1
1010
+ } else if (char === "/" && next === ">") {
1011
+ stack.pop()
1012
+ expression = false
1013
+ index += 2
1014
+ } else {
1015
+ if (char === ">") stack[stack.length - 1] = "children"
1016
+ index += 1
1017
+ }
1018
+ } else if (char === "/" && next === "/") {
1019
+ const end = source.indexOf("\n", index)
1020
+ index = end === -1 ? length : end
1021
+ } else if (char === "/" && next === "*") {
1022
+ const end = source.indexOf("*/", index + 2)
1023
+ index = end === -1 ? length : end + 2
1024
+ } else if (char === '"' || char === "'") {
1025
+ skipQuoted(char, false)
1026
+ expression = false
1027
+ } else if (char === "`") {
1028
+ stack.push("template")
1029
+ index += 1
1030
+ } else if (char === "/" && expression) {
1031
+ let inClass = false
1032
+ index += 1
1033
+ while (index < length && source[index] !== "\n") {
1034
+ const current = source[index]
1035
+ if (current === "\\") index += 1
1036
+ else if (current === "[") inClass = true
1037
+ else if (current === "]") inClass = false
1038
+ else if (current === "/" && !inClass) break
1039
+ index += 1
1040
+ }
1041
+ index += 1
1042
+ while (index < length && WORD_CHAR.test(source[index] ?? "")) index += 1
1043
+ expression = false
1044
+ } else if (char === "<" && expression && jsx && opensElement(source, index)) {
1045
+ stack.push("tag")
1046
+ index += 1
1047
+ } else if (WORD_CHAR.test(char)) {
1048
+ const start = index
1049
+ while (index < length && WORD_CHAR.test(source[index] ?? "")) index += 1
1050
+ expression = EXPRESSION_KEYWORDS.has(source.slice(start, index))
1051
+ } else {
1052
+ if (char === "{") stack.push("code")
1053
+ else if (char === "}" && stack.length > 1) stack.pop()
1054
+ if (char.trim() !== "") expression = char !== ")" && char !== "]"
1055
+ index += 1
1056
+ }
1057
+ }
1058
+ return mask
1059
+ }
1060
+
1061
+ const classAttributes = (source: string, file: string): readonly ClassAttribute[] => {
937
1062
  const attributes: ClassAttribute[] = []
1063
+ // A TypeScript module that is not `.tsx` cannot hold JSX, and its `<T>` would read as an element.
1064
+ const inTag = jsxTagMask(source, !/\.[cm]?ts$/.test(file))
938
1065
  for (const match of source.matchAll(CLASS_ATTRIBUTE)) {
939
1066
  const start = match.index ?? 0
940
- if (!isLikelyJsxAttribute(source, start)) continue
1067
+ if (inTag[start] !== 1) continue
941
1068
  const end = start + match[0].length
942
1069
  const value = match[1] ?? match[2] ?? match[3] ?? match[4]
943
1070
  attributes.push(
@@ -972,32 +1099,131 @@ const existingStylexImport = (source: string): string | undefined => {
972
1099
  return match?.[1]
973
1100
  }
974
1101
 
975
- const insertionOffset = (source: string): number => {
976
- let offset = source.startsWith("#!") ? source.indexOf("\n") + 1 || source.length : 0
977
- const lines = source.slice(offset).split(/(?<=\n)/)
978
- let consumed = 0
979
- let inImport = false
980
- for (const line of lines) {
981
- const trimmed = line.trim()
982
- const directive = /^(['"])(?:use client|use server)\1;?$/.test(trimmed)
983
- if (inImport || trimmed.startsWith("import ") || trimmed.startsWith("import{")) {
984
- inImport = !/(?:from\s+["'][^"']+["']|import\s+["'][^"']+["'])\s*;?$/.test(trimmed)
985
- consumed += line.length
1102
+ const isIdentifierChar = (char: string | undefined): boolean =>
1103
+ char !== undefined && /[\w$]/.test(char)
1104
+
1105
+ /** Past the whitespace and comments at `index`. `inLine` stops at the end of the line. */
1106
+ function skipTrivia(source: string, index: number, inLine = false): number {
1107
+ let at = index
1108
+ while (at < source.length) {
1109
+ const char = source[at]
1110
+ if (char === " " || char === "\t" || char === "\r" || (char === "\n" && !inLine)) at++
1111
+ else if (source.startsWith("//", at)) {
1112
+ const end = source.indexOf("\n", at)
1113
+ if (end === -1) return source.length
1114
+ at = inLine ? end : end + 1
1115
+ } else if (source.startsWith("/*", at)) {
1116
+ const end = source.indexOf("*/", at + 2)
1117
+ if (end === -1) return source.length
1118
+ at = end + 2
1119
+ } else break
1120
+ }
1121
+ return at
1122
+ }
1123
+
1124
+ function skipString(source: string, index: number): number {
1125
+ const quote = source[index]
1126
+ let at = index + 1
1127
+ while (at < source.length && source[at] !== quote && source[at] !== "\n")
1128
+ at += source[at] === "\\" ? 2 : 1
1129
+ return Math.min(at + 1, source.length)
1130
+ }
1131
+
1132
+ /** Past an import's specifier: its `with { ... }` (or `assert`) attributes and a `;`. */
1133
+ function afterSpecifier(source: string, index: number): number {
1134
+ let at = index
1135
+ const next = skipTrivia(source, at, true)
1136
+ for (const keyword of ["with", "assert"]) {
1137
+ if (!source.startsWith(keyword, next) || isIdentifierChar(source[next + keyword.length]))
1138
+ continue
1139
+ const open = skipTrivia(source, next + keyword.length)
1140
+ const close = source[open] === "{" ? source.indexOf("}", open) : -1
1141
+ if (close !== -1) at = close + 1
1142
+ }
1143
+ const semicolon = skipTrivia(source, at, true)
1144
+ return source[semicolon] === ";" ? semicolon + 1 : at
1145
+ }
1146
+
1147
+ /** Where the import declaration whose `import` keyword is at `index` ends, or `undefined` for an
1148
+ * `import(...)` / `import.meta` expression or a declaration this scan cannot read. */
1149
+ function importEnd(source: string, index: number): number | undefined {
1150
+ let at = skipTrivia(source, index + "import".length)
1151
+ if (source[at] === "(" || source[at] === ".") return undefined
1152
+ if (source[at] === '"' || source[at] === "'")
1153
+ return afterSpecifier(source, skipString(source, at))
1154
+ let depth = 0
1155
+ while (at < source.length) {
1156
+ const char = source[at]
1157
+ if (char === '"' || char === "'") {
1158
+ at = skipString(source, at)
986
1159
  continue
987
1160
  }
988
- if (directive || trimmed === "") {
989
- consumed += line.length
1161
+ if (source.startsWith("//", at) || source.startsWith("/*", at)) {
1162
+ at = skipTrivia(source, at)
990
1163
  continue
991
1164
  }
992
- break
1165
+ if (char === "{") depth++
1166
+ else if (char === "}") depth--
1167
+ else if (depth === 0 && (char === ";" || char === "=")) return undefined
1168
+ else if (
1169
+ depth === 0 &&
1170
+ source.startsWith("from", at) &&
1171
+ !isIdentifierChar(source[at - 1]) &&
1172
+ !isIdentifierChar(source[at + 4])
1173
+ ) {
1174
+ const specifier = skipTrivia(source, at + 4)
1175
+ if (source[specifier] === '"' || source[specifier] === "'")
1176
+ return afterSpecifier(source, skipString(source, specifier))
1177
+ }
1178
+ at++
1179
+ }
1180
+ return undefined
1181
+ }
1182
+
1183
+ /** Past the rest of the line `index` is on when only trivia is left on it, then any blank lines. */
1184
+ function nextLine(source: string, index: number): number {
1185
+ let at = skipTrivia(source, index, true)
1186
+ if (at < source.length && source[at] !== "\n") return index
1187
+ for (;;) {
1188
+ if (source[at] !== "\n") return at
1189
+ let next = at + 1
1190
+ while (source[next] === " " || source[next] === "\t" || source[next] === "\r") next++
1191
+ if (next < source.length && source[next] !== "\n") return at + 1
1192
+ at = next
1193
+ }
1194
+ }
1195
+
1196
+ /**
1197
+ * Where the generated block goes: after the directive prologue and the import declarations. Comments
1198
+ * are read as comments, so one before `"use client"` keeps the directive first, and a trailing
1199
+ * `// note` on the last import does not hide where it ends.
1200
+ */
1201
+ const insertionOffset = (source: string): number => {
1202
+ let end = source.startsWith("#!") ? source.indexOf("\n") + 1 || source.length : 0
1203
+ let prologue = true
1204
+ for (;;) {
1205
+ const at = skipTrivia(source, end)
1206
+ const char = source[at]
1207
+ let statementEnd: number | undefined
1208
+ if (prologue && (char === '"' || char === "'")) {
1209
+ const literal = skipString(source, at)
1210
+ const after = skipTrivia(source, literal, true)
1211
+ // A string with more of an expression after it on its line is not a directive.
1212
+ if (source[after] === ";") statementEnd = after + 1
1213
+ else if (after === source.length || source[after] === "\n") statementEnd = literal
1214
+ } else if (source.startsWith("import", at) && !isIdentifierChar(source[at + 6])) {
1215
+ prologue = false
1216
+ statementEnd = importEnd(source, at)
1217
+ }
1218
+ if (statementEnd === undefined) break
1219
+ end = nextLine(source, statementEnd)
993
1220
  }
994
- offset += consumed
995
- return offset
1221
+ return end
996
1222
  }
997
1223
 
998
1224
  /** Transform one source file. The returned source is unchanged when every match is unsupported. */
999
1225
  export function migrateTailwindSource(source: string, file: string): SourceMigration {
1000
- const attributes = classAttributes(source)
1226
+ const attributes = classAttributes(source, file)
1001
1227
  if (attributes.length === 0) return { source, changed: false, replacements: 0, issues: [] }
1002
1228
 
1003
1229
  const replacements: { start: number; end: number; text: string }[] = []
@@ -1069,7 +1295,8 @@ export function migrateTailwindSource(source: string, file: string): SourceMigra
1069
1295
  const needsImport = existingStylexImport(source) === undefined
1070
1296
  const generated = `${needsImport ? `import * as ${stylexName} from "${STYLEX_SPECIFIER}"\n\n` : ""}const ${stylesName} = ${stylexName}.create(${JSON.stringify(Object.fromEntries(styles), null, 2)})\n\n`
1071
1297
  const offset = insertionOffset(rewritten)
1072
- const withGenerated = `${rewritten.slice(0, offset)}${generated}${rewritten.slice(offset)}`
1298
+ const separator = offset > 0 && rewritten[offset - 1] !== "\n" ? "\n" : ""
1299
+ const withGenerated = `${rewritten.slice(0, offset)}${separator}${generated}${rewritten.slice(offset)}`
1073
1300
  return { source: withGenerated, changed: true, replacements: replacements.length, issues }
1074
1301
  }
1075
1302
 
package/src/test-tool.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { type HeadTailOutput, readHeadTail } from "./mcp-io.ts"
1
+ import {
2
+ escalatingStop,
3
+ type HeadTailOutput,
4
+ OWN_PROCESS_GROUP,
5
+ readHeadTail,
6
+ signalProcessGroup,
7
+ } from "./mcp-io.ts"
2
8
  import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.ts"
3
9
 
4
10
  export interface TestToolArgs {
@@ -146,17 +152,19 @@ export async function collectTestResult(
146
152
  env: process.env,
147
153
  stdout: "pipe",
148
154
  stderr: "pipe",
155
+ detached: OWN_PROCESS_GROUP,
149
156
  })
150
157
  let timedOut = false
151
158
  let cancelled = false
159
+ const { stop, dispose } = escalatingStop((name) => signalProcessGroup(proc, name))
152
160
  const abort = (): void => {
153
161
  cancelled = true
154
- proc.kill()
162
+ stop()
155
163
  }
156
164
  opts.signal?.addEventListener("abort", abort, { once: true })
157
165
  const timer = setTimeout(() => {
158
166
  timedOut = true
159
- proc.kill()
167
+ stop()
160
168
  }, timeoutMs)
161
169
  const empty: HeadTailOutput = { head: "", tail: "", droppedBytes: 0 }
162
170
  let stdoutRaw = empty
@@ -175,6 +183,7 @@ export async function collectTestResult(
175
183
  exitCode = result[2]
176
184
  } finally {
177
185
  clearTimeout(timer)
186
+ dispose()
178
187
  opts.signal?.removeEventListener("abort", abort)
179
188
  }
180
189
  const stdout = clean(stdoutRaw)
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Which zoned files the typecheck never read. `tsc --noEmit` passes for code outside its program, so
3
+ * a stale tsconfig `include` (a folder moved under `backend/`, say) turns the contract gate into a
4
+ * pass that checked nothing there. The check runs tsc with `--listFiles` and compares the program
5
+ * against the app's zone folders.
6
+ */
7
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
8
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
9
+ import { stripComments } from "./check-scan.ts"
10
+
11
+ const ZONE_FOLDER = new Set(["routes", "frontend", "backend", "shared"])
12
+ const TS_SOURCE = /\.(?:[cm]?ts|tsx)$/
13
+ const NOT_PROGRAM_CODE = /\.d\.[cm]?ts$|\.(?:test|spec)\.[cm]?tsx?$/
14
+ const TSC_DIAGNOSTIC = /\(\d+,\d+\):\s*(?:error|warning)\s+TS\d+:/
15
+
16
+ /** Split `tsc --listFiles` output into the program's files and the compiler's own output. */
17
+ export function splitListedFiles(output: string): {
18
+ readonly programFiles: readonly string[]
19
+ readonly output: string
20
+ } {
21
+ const programFiles: string[] = []
22
+ const rest: string[] = []
23
+ for (const line of output.split(/\r?\n/)) {
24
+ if (line !== "" && line === line.trim() && isAbsolute(line) && !TSC_DIAGNOSTIC.test(line))
25
+ programFiles.push(line)
26
+ else rest.push(line)
27
+ }
28
+ return { programFiles, output: rest.join("\n").trim() }
29
+ }
30
+
31
+ const real = (path: string): string => {
32
+ try {
33
+ return realpathSync.native(path)
34
+ } catch {
35
+ return path
36
+ }
37
+ }
38
+
39
+ // tsc prints forward slashes, and on Windows the drive letter's case is not stable.
40
+ const canonical = (path: string): string => {
41
+ const posix = path.replaceAll("\\", "/")
42
+ return process.platform === "win32" ? posix.toLowerCase() : posix
43
+ }
44
+
45
+ /** The zone folder a project file sits in (`backend`, `apps/web/routes`), or `undefined`. */
46
+ export function zoneFolderOf(file: string): string | undefined {
47
+ const parts = file.split("/")
48
+ const at = parts.findIndex((part, i) => i < parts.length - 1 && ZONE_FOLDER.has(part))
49
+ return at === -1 ? undefined : parts.slice(0, at + 1).join("/")
50
+ }
51
+
52
+ /** True when a tsconfig between the app root and `cwd` makes the app its own program. */
53
+ function hasOwnProgram(cwd: string, app: string): boolean {
54
+ let dir = app
55
+ while (dir !== "" && dir !== ".") {
56
+ if (existsSync(join(cwd, dir, "tsconfig.json"))) return true
57
+ const parent = dirname(dir)
58
+ if (parent === dir) return false
59
+ dir = parent
60
+ }
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * The `exclude` patterns of `<cwd>/tsconfig.json`, inherited through relative `extends` as tsc does,
66
+ * rewritten relative to `cwd`. A file a tsconfig excludes is out of the program on purpose.
67
+ */
68
+ export function tsconfigExcludes(cwd: string): string[] {
69
+ let path = join(cwd, "tsconfig.json")
70
+ for (let depth = 0; depth < 8 && existsSync(path); depth++) {
71
+ let config: unknown
72
+ try {
73
+ config = JSON.parse(stripComments(readFileSync(path, "utf8")).replace(/,(\s*[}\]])/g, "$1"))
74
+ } catch {
75
+ return []
76
+ }
77
+ if (typeof config !== "object" || config === null) return []
78
+ const exclude: unknown = Reflect.get(config, "exclude")
79
+ if (Array.isArray(exclude)) {
80
+ const from = dirname(path)
81
+ return exclude
82
+ .filter((pattern): pattern is string => typeof pattern === "string")
83
+ .map((pattern) => relative(cwd, resolve(from, pattern)).replaceAll("\\", "/"))
84
+ }
85
+ const base: unknown = Reflect.get(config, "extends")
86
+ if (typeof base !== "string" || !base.startsWith(".")) return []
87
+ path = resolve(dirname(path), base.endsWith(".json") ? base : `${base}.json`)
88
+ }
89
+ return []
90
+ }
91
+
92
+ // tsc reads a pattern that names a folder as everything below it.
93
+ const excludedBy = (patterns: readonly string[], file: string): boolean =>
94
+ patterns.some(
95
+ (pattern) => new Bun.Glob(pattern).match(file) || new Bun.Glob(`${pattern}/**`).match(file),
96
+ )
97
+
98
+ /**
99
+ * The TypeScript files under a zone folder that `programFiles` does not contain, project-relative and
100
+ * in input order. Declaration files and tests are left out: a project may typecheck tests with a
101
+ * separate config. So is an app nested under its own tsconfig, which this program does not own, and a
102
+ * file the tsconfig's `exclude` names.
103
+ */
104
+ export function filesOutsideProgram(
105
+ cwd: string,
106
+ files: readonly string[],
107
+ programFiles: readonly string[],
108
+ ): string[] {
109
+ const program = new Set<string>()
110
+ for (const file of programFiles) {
111
+ program.add(canonical(file))
112
+ if (!file.includes("/node_modules/")) program.add(canonical(real(file)))
113
+ }
114
+ const ownProgram = new Map<string, boolean>()
115
+ const excluded = tsconfigExcludes(cwd)
116
+ const outside: string[] = []
117
+ for (const file of files) {
118
+ if (!TS_SOURCE.test(file) || NOT_PROGRAM_CODE.test(file)) continue
119
+ if (excludedBy(excluded, file)) continue
120
+ const folder = zoneFolderOf(file)
121
+ if (folder === undefined) continue
122
+ const app = dirname(folder)
123
+ let own = ownProgram.get(app)
124
+ if (own === undefined) {
125
+ own = hasOwnProgram(cwd, app)
126
+ ownProgram.set(app, own)
127
+ }
128
+ if (own) continue
129
+ const abs = join(cwd, file)
130
+ if (program.has(canonical(abs)) || program.has(canonical(real(abs)))) continue
131
+ outside.push(file)
132
+ }
133
+ return outside
134
+ }