@shivaedev/quality 0.4.0 → 0.5.1

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 (387) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +119 -29
  3. package/biome/declarations.json +17 -0
  4. package/biome/plugins/ambient-runtime.grit +18 -0
  5. package/biome/plugins/effect-fn-spans.grit +7 -0
  6. package/biome/preset.json +602 -0
  7. package/dist/baseline/compare.d.ts +8 -9
  8. package/dist/baseline/compare.d.ts.map +1 -1
  9. package/dist/baseline/compare.js +4 -4
  10. package/dist/baseline/compare.js.map +1 -1
  11. package/dist/baseline/format.d.ts +4 -4
  12. package/dist/baseline/format.d.ts.map +1 -1
  13. package/dist/baseline/format.js +3 -2
  14. package/dist/baseline/format.js.map +1 -1
  15. package/dist/baseline/guard.d.ts +3 -3
  16. package/dist/baseline/guard.d.ts.map +1 -1
  17. package/dist/baseline/guard.js +6 -5
  18. package/dist/baseline/guard.js.map +1 -1
  19. package/dist/baseline/legacy.d.ts +3 -3
  20. package/dist/baseline/legacy.d.ts.map +1 -1
  21. package/dist/baseline/legacy.js.map +1 -1
  22. package/dist/baseline/rewrite.d.ts +1 -1
  23. package/dist/baseline/rewrite.d.ts.map +1 -1
  24. package/dist/baseline/rewrite.js.map +1 -1
  25. package/dist/baseline/update.d.ts +8 -9
  26. package/dist/baseline/update.d.ts.map +1 -1
  27. package/dist/baseline/update.js +10 -9
  28. package/dist/baseline/update.js.map +1 -1
  29. package/dist/biome/report.d.ts +33 -0
  30. package/dist/biome/report.d.ts.map +1 -0
  31. package/dist/biome/report.js +25 -0
  32. package/dist/biome/report.js.map +1 -0
  33. package/dist/biome/run.d.ts +7 -0
  34. package/dist/biome/run.d.ts.map +1 -0
  35. package/dist/biome/run.js +16 -0
  36. package/dist/biome/run.js.map +1 -0
  37. package/dist/cli/args.d.ts +7 -3
  38. package/dist/cli/args.d.ts.map +1 -1
  39. package/dist/cli/args.js +12 -5
  40. package/dist/cli/args.js.map +1 -1
  41. package/dist/cli/baseline.d.ts +1 -1
  42. package/dist/cli/baseline.d.ts.map +1 -1
  43. package/dist/cli/baseline.js +3 -3
  44. package/dist/cli/baseline.js.map +1 -1
  45. package/dist/cli/check.js.map +1 -1
  46. package/dist/cli/fix.d.ts +4 -0
  47. package/dist/cli/fix.d.ts.map +1 -0
  48. package/dist/cli/fix.js +21 -0
  49. package/dist/cli/fix.js.map +1 -0
  50. package/dist/cli/lint.js +1 -1
  51. package/dist/cli/lint.js.map +1 -1
  52. package/dist/cli/run-main.d.ts.map +1 -1
  53. package/dist/cli/run-main.js +7 -3
  54. package/dist/cli/run-main.js.map +1 -1
  55. package/dist/cli.js +3 -0
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config/decode.d.ts +4 -4
  58. package/dist/config/decode.js +12 -12
  59. package/dist/config/decode.js.map +1 -1
  60. package/dist/config/load.d.ts +3 -3
  61. package/dist/config/load.d.ts.map +1 -1
  62. package/dist/config/load.js +1 -1
  63. package/dist/config/load.js.map +1 -1
  64. package/dist/config/resolve.d.ts +3 -5
  65. package/dist/config/resolve.d.ts.map +1 -1
  66. package/dist/config/resolve.js +11 -5
  67. package/dist/config/resolve.js.map +1 -1
  68. package/dist/config.d.ts +9 -9
  69. package/dist/config.d.ts.map +1 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/decoded.d.ts +1 -1
  72. package/dist/decoded.d.ts.map +1 -1
  73. package/dist/engine/baseline-file.d.ts +1 -1
  74. package/dist/engine/baseline-file.d.ts.map +1 -1
  75. package/dist/engine/evaluate.d.ts +8 -9
  76. package/dist/engine/evaluate.d.ts.map +1 -1
  77. package/dist/engine/evaluate.js +4 -4
  78. package/dist/engine/evaluate.js.map +1 -1
  79. package/dist/engine/run-rules.d.ts +4 -3
  80. package/dist/engine/run-rules.d.ts.map +1 -1
  81. package/dist/engine/run-rules.js +3 -3
  82. package/dist/engine/run-rules.js.map +1 -1
  83. package/dist/engine/session.d.ts +3 -3
  84. package/dist/engine/session.d.ts.map +1 -1
  85. package/dist/engine/violation.d.ts +11 -3
  86. package/dist/engine/violation.d.ts.map +1 -1
  87. package/dist/engine/violation.js +6 -2
  88. package/dist/engine/violation.js.map +1 -1
  89. package/dist/exceptions/registry.d.ts +7 -8
  90. package/dist/exceptions/registry.d.ts.map +1 -1
  91. package/dist/exceptions/registry.js +6 -6
  92. package/dist/exceptions/registry.js.map +1 -1
  93. package/dist/git/base.d.ts +1 -1
  94. package/dist/git/base.d.ts.map +1 -1
  95. package/dist/git/base.js.map +1 -1
  96. package/dist/git/command.d.ts +3 -3
  97. package/dist/git/command.d.ts.map +1 -1
  98. package/dist/git/command.js.map +1 -1
  99. package/dist/git/history.d.ts +1 -1
  100. package/dist/git/history.d.ts.map +1 -1
  101. package/dist/git/history.js +1 -1
  102. package/dist/git/history.js.map +1 -1
  103. package/dist/imports/ambient.d.ts +7 -0
  104. package/dist/imports/ambient.d.ts.map +1 -0
  105. package/dist/imports/ambient.js +44 -0
  106. package/dist/imports/ambient.js.map +1 -0
  107. package/dist/imports/components.d.ts +2 -0
  108. package/dist/imports/components.d.ts.map +1 -0
  109. package/dist/imports/components.js +63 -0
  110. package/dist/imports/components.js.map +1 -0
  111. package/dist/imports/extract.d.ts +10 -0
  112. package/dist/imports/extract.d.ts.map +1 -0
  113. package/dist/imports/extract.js +100 -0
  114. package/dist/imports/extract.js.map +1 -0
  115. package/dist/imports/fences/dsl.d.ts +18 -0
  116. package/dist/imports/fences/dsl.d.ts.map +1 -0
  117. package/dist/imports/fences/dsl.js +14 -0
  118. package/dist/imports/fences/dsl.js.map +1 -0
  119. package/dist/imports/fences/evaluate.d.ts +12 -0
  120. package/dist/imports/fences/evaluate.d.ts.map +1 -0
  121. package/dist/imports/fences/evaluate.js +55 -0
  122. package/dist/imports/fences/evaluate.js.map +1 -0
  123. package/dist/imports/fences/match.d.ts +17 -0
  124. package/dist/imports/fences/match.d.ts.map +1 -0
  125. package/dist/imports/fences/match.js +106 -0
  126. package/dist/imports/fences/match.js.map +1 -0
  127. package/dist/imports/fences/model.d.ts +60 -0
  128. package/dist/imports/fences/model.d.ts.map +1 -0
  129. package/dist/imports/fences/model.js +2 -0
  130. package/dist/imports/fences/model.js.map +1 -0
  131. package/dist/imports/fences/policy.d.ts +5 -0
  132. package/dist/imports/fences/policy.d.ts.map +1 -0
  133. package/dist/imports/fences/policy.js +59 -0
  134. package/dist/imports/fences/policy.js.map +1 -0
  135. package/dist/imports/fences/reach.d.ts +12 -0
  136. package/dist/imports/fences/reach.d.ts.map +1 -0
  137. package/dist/imports/fences/reach.js +39 -0
  138. package/dist/imports/fences/reach.js.map +1 -0
  139. package/dist/imports/fences/selectors.d.ts +11 -0
  140. package/dist/imports/fences/selectors.d.ts.map +1 -0
  141. package/dist/imports/fences/selectors.js +27 -0
  142. package/dist/imports/fences/selectors.js.map +1 -0
  143. package/dist/imports/fences/vocabulary.d.ts +9 -0
  144. package/dist/imports/fences/vocabulary.d.ts.map +1 -0
  145. package/dist/imports/fences/vocabulary.js +40 -0
  146. package/dist/imports/fences/vocabulary.js.map +1 -0
  147. package/dist/imports/generated.d.ts +6 -0
  148. package/dist/imports/generated.d.ts.map +1 -0
  149. package/dist/imports/generated.js +78 -0
  150. package/dist/imports/generated.js.map +1 -0
  151. package/dist/imports/glob.d.ts +2 -0
  152. package/dist/imports/glob.d.ts.map +1 -0
  153. package/dist/imports/glob.js +125 -0
  154. package/dist/imports/glob.js.map +1 -0
  155. package/dist/imports/graph.d.ts +23 -0
  156. package/dist/imports/graph.d.ts.map +1 -0
  157. package/dist/imports/graph.js +56 -0
  158. package/dist/imports/graph.js.map +1 -0
  159. package/dist/imports/paths.d.ts +2 -0
  160. package/dist/imports/paths.d.ts.map +1 -0
  161. package/dist/imports/paths.js +24 -0
  162. package/dist/imports/paths.js.map +1 -0
  163. package/dist/imports/pnpm-workspace.d.ts +2 -0
  164. package/dist/imports/pnpm-workspace.d.ts.map +1 -0
  165. package/dist/imports/pnpm-workspace.js +122 -0
  166. package/dist/imports/pnpm-workspace.js.map +1 -0
  167. package/dist/imports/projects.d.ts +10 -0
  168. package/dist/imports/projects.d.ts.map +1 -0
  169. package/dist/imports/projects.js +83 -0
  170. package/dist/imports/projects.js.map +1 -0
  171. package/dist/imports/resolve.d.ts +16 -0
  172. package/dist/imports/resolve.d.ts.map +1 -0
  173. package/dist/imports/resolve.js +88 -0
  174. package/dist/imports/resolve.js.map +1 -0
  175. package/dist/imports/workspace.d.ts +10 -0
  176. package/dist/imports/workspace.d.ts.map +1 -0
  177. package/dist/imports/workspace.js +55 -0
  178. package/dist/imports/workspace.js.map +1 -0
  179. package/dist/index.d.ts +3 -0
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +2 -0
  182. package/dist/index.js.map +1 -1
  183. package/dist/inventory/collect.d.ts +6 -6
  184. package/dist/inventory/collect.d.ts.map +1 -1
  185. package/dist/inventory/collect.js.map +1 -1
  186. package/dist/manifests/sorted.d.ts +14 -0
  187. package/dist/manifests/sorted.d.ts.map +1 -0
  188. package/dist/manifests/sorted.js +39 -0
  189. package/dist/manifests/sorted.js.map +1 -0
  190. package/dist/report/guard.d.ts +1 -1
  191. package/dist/report/guard.d.ts.map +1 -1
  192. package/dist/report/guard.js +1 -1
  193. package/dist/report/guard.js.map +1 -1
  194. package/dist/report/render.d.ts +2 -2
  195. package/dist/report/render.d.ts.map +1 -1
  196. package/dist/report/render.js.map +1 -1
  197. package/dist/report/stale.d.ts +2 -2
  198. package/dist/report/stale.d.ts.map +1 -1
  199. package/dist/report/stale.js.map +1 -1
  200. package/dist/rule.d.ts +12 -11
  201. package/dist/rule.d.ts.map +1 -1
  202. package/dist/rule.js.map +1 -1
  203. package/dist/rules/biome.d.ts +4 -0
  204. package/dist/rules/biome.d.ts.map +1 -0
  205. package/dist/rules/biome.js +40 -0
  206. package/dist/rules/biome.js.map +1 -0
  207. package/dist/rules/built-in.d.ts +8 -10
  208. package/dist/rules/built-in.d.ts.map +1 -1
  209. package/dist/rules/built-in.js +12 -0
  210. package/dist/rules/built-in.js.map +1 -1
  211. package/dist/rules/comments/kinds.d.ts +1 -2
  212. package/dist/rules/comments/kinds.d.ts.map +1 -1
  213. package/dist/rules/comments/kinds.js +7 -16
  214. package/dist/rules/comments/kinds.js.map +1 -1
  215. package/dist/rules/comments/max-per-file.d.ts +1 -1
  216. package/dist/rules/comments/max-per-file.js +11 -11
  217. package/dist/rules/comments/max-per-file.js.map +1 -1
  218. package/dist/rules/comments/no-banner.js +2 -2
  219. package/dist/rules/comments/no-banner.js.map +1 -1
  220. package/dist/rules/comments/no-environment-pragma.d.ts +2 -0
  221. package/dist/rules/comments/no-environment-pragma.d.ts.map +1 -0
  222. package/dist/rules/comments/no-environment-pragma.js +8 -0
  223. package/dist/rules/comments/no-environment-pragma.js.map +1 -0
  224. package/dist/rules/comments/no-jsdoc.js +3 -3
  225. package/dist/rules/comments/no-jsdoc.js.map +1 -1
  226. package/dist/rules/comments/no-line-reference.js +2 -2
  227. package/dist/rules/comments/no-line-reference.js.map +1 -1
  228. package/dist/rules/comments/no-pr-reference.js +6 -6
  229. package/dist/rules/comments/no-pr-reference.js.map +1 -1
  230. package/dist/rules/comments/no-todo.js +2 -2
  231. package/dist/rules/comments/no-todo.js.map +1 -1
  232. package/dist/rules/comments/pattern-rule.d.ts +2 -2
  233. package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
  234. package/dist/rules/comments/pattern-rule.js +2 -2
  235. package/dist/rules/comments/pattern-rule.js.map +1 -1
  236. package/dist/rules/comments/scan.d.ts +6 -6
  237. package/dist/rules/comments/scan.d.ts.map +1 -1
  238. package/dist/rules/comments/scan.js +2 -2
  239. package/dist/rules/comments/scan.js.map +1 -1
  240. package/dist/rules/imports/cycles.d.ts +2 -0
  241. package/dist/rules/imports/cycles.d.ts.map +1 -0
  242. package/dist/rules/imports/cycles.js +38 -0
  243. package/dist/rules/imports/cycles.js.map +1 -0
  244. package/dist/rules/imports/fences.d.ts +5 -0
  245. package/dist/rules/imports/fences.d.ts.map +1 -0
  246. package/dist/rules/imports/fences.js +24 -0
  247. package/dist/rules/imports/fences.js.map +1 -0
  248. package/dist/rules/imports/resolvable.d.ts +4 -0
  249. package/dist/rules/imports/resolvable.d.ts.map +1 -0
  250. package/dist/rules/imports/resolvable.js +22 -0
  251. package/dist/rules/imports/resolvable.js.map +1 -0
  252. package/dist/rules/manifests-sorted.d.ts +2 -0
  253. package/dist/rules/manifests-sorted.d.ts.map +1 -0
  254. package/dist/rules/manifests-sorted.js +22 -0
  255. package/dist/rules/manifests-sorted.js.map +1 -0
  256. package/dist/rules/max-lines.js +4 -4
  257. package/dist/rules/max-lines.js.map +1 -1
  258. package/dist/rules/suppressions/biome/configs.d.ts +6 -6
  259. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  260. package/dist/rules/suppressions/biome/configs.js +2 -2
  261. package/dist/rules/suppressions/biome/configs.js.map +1 -1
  262. package/dist/rules/suppressions/biome/declarations.d.ts +4 -4
  263. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -1
  264. package/dist/rules/suppressions/biome/declarations.js +3 -3
  265. package/dist/rules/suppressions/biome/declarations.js.map +1 -1
  266. package/dist/rules/suppressions/biome/json.d.ts +2 -2
  267. package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
  268. package/dist/rules/suppressions/biome/json.js +1 -1
  269. package/dist/rules/suppressions/biome/json.js.map +1 -1
  270. package/dist/rules/suppressions/biome/resolve.js +3 -3
  271. package/dist/rules/suppressions/biome/resolve.js.map +1 -1
  272. package/dist/rules/suppressions/biome/weakenings.d.ts +4 -4
  273. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  274. package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
  275. package/dist/rules/suppressions/biome-overrides.d.ts +1 -1
  276. package/dist/rules/suppressions/biome-overrides.js +4 -4
  277. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  278. package/dist/rules/suppressions/directives.d.ts +1 -1
  279. package/dist/rules/suppressions/directives.d.ts.map +1 -1
  280. package/dist/rules/suppressions/directives.js +11 -11
  281. package/dist/rules/suppressions/directives.js.map +1 -1
  282. package/dist/rules/suppressions/no-double-cast.js +2 -2
  283. package/dist/rules/suppressions/no-double-cast.js.map +1 -1
  284. package/dist/rules/suppressions/no-inline.d.ts +1 -7
  285. package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
  286. package/dist/rules/suppressions/no-inline.js +7 -30
  287. package/dist/rules/suppressions/no-inline.js.map +1 -1
  288. package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
  289. package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
  290. package/dist/rules/suppressions/stylesheet-comments.js +3 -3
  291. package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
  292. package/dist/rules/syntax.js +4 -4
  293. package/dist/rules/syntax.js.map +1 -1
  294. package/dist/standard-schema.d.ts +1 -1
  295. package/dist/standard-schema.d.ts.map +1 -1
  296. package/dist/vitest.d.ts +3 -0
  297. package/dist/vitest.d.ts.map +1 -0
  298. package/dist/vitest.js +20 -0
  299. package/dist/vitest.js.map +1 -0
  300. package/package.json +46 -31
  301. package/src/baseline/compare.ts +10 -15
  302. package/src/baseline/format.ts +8 -6
  303. package/src/baseline/guard.ts +11 -10
  304. package/src/baseline/legacy.ts +3 -3
  305. package/src/baseline/rewrite.ts +1 -1
  306. package/src/baseline/update.ts +21 -21
  307. package/src/biome/report.ts +35 -0
  308. package/src/biome/run.ts +23 -0
  309. package/src/cli/args.ts +16 -8
  310. package/src/cli/baseline.ts +5 -6
  311. package/src/cli/check.ts +1 -1
  312. package/src/cli/fix.ts +23 -0
  313. package/src/cli/lint.ts +1 -1
  314. package/src/cli/run-main.ts +8 -3
  315. package/src/cli.ts +3 -0
  316. package/src/config/decode.ts +12 -12
  317. package/src/config/load.ts +4 -4
  318. package/src/config/resolve.ts +15 -11
  319. package/src/config.ts +9 -9
  320. package/src/decoded.ts +1 -1
  321. package/src/engine/baseline-file.ts +1 -1
  322. package/src/engine/evaluate.ts +14 -16
  323. package/src/engine/run-rules.ts +8 -7
  324. package/src/engine/session.ts +3 -3
  325. package/src/engine/violation.ts +18 -4
  326. package/src/exceptions/registry.ts +10 -16
  327. package/src/git/base.ts +2 -2
  328. package/src/git/command.ts +3 -3
  329. package/src/git/history.ts +2 -2
  330. package/src/imports/ambient.ts +54 -0
  331. package/src/imports/components.ts +80 -0
  332. package/src/imports/extract.ts +129 -0
  333. package/src/imports/fences/dsl.ts +33 -0
  334. package/src/imports/fences/evaluate.ts +75 -0
  335. package/src/imports/fences/match.ts +148 -0
  336. package/src/imports/fences/model.ts +42 -0
  337. package/src/imports/fences/policy.ts +70 -0
  338. package/src/imports/fences/reach.ts +62 -0
  339. package/src/imports/fences/selectors.ts +37 -0
  340. package/src/imports/fences/vocabulary.ts +51 -0
  341. package/src/imports/generated.ts +95 -0
  342. package/src/imports/glob.ts +142 -0
  343. package/src/imports/graph.ts +81 -0
  344. package/src/imports/paths.ts +28 -0
  345. package/src/imports/pnpm-workspace.ts +134 -0
  346. package/src/imports/projects.ts +106 -0
  347. package/src/imports/resolve.ts +115 -0
  348. package/src/imports/workspace.ts +76 -0
  349. package/src/index.ts +3 -0
  350. package/src/inventory/collect.ts +6 -6
  351. package/src/manifests/sorted.ts +46 -0
  352. package/src/report/guard.ts +2 -2
  353. package/src/report/render.ts +9 -9
  354. package/src/report/stale.ts +3 -3
  355. package/src/rule.ts +13 -12
  356. package/src/rules/biome.ts +48 -0
  357. package/src/rules/built-in.ts +12 -0
  358. package/src/rules/comments/kinds.ts +11 -21
  359. package/src/rules/comments/max-per-file.ts +13 -13
  360. package/src/rules/comments/no-banner.ts +2 -2
  361. package/src/rules/comments/no-environment-pragma.ts +9 -0
  362. package/src/rules/comments/no-jsdoc.ts +3 -3
  363. package/src/rules/comments/no-line-reference.ts +2 -2
  364. package/src/rules/comments/no-pr-reference.ts +6 -6
  365. package/src/rules/comments/no-todo.ts +2 -2
  366. package/src/rules/comments/pattern-rule.ts +6 -6
  367. package/src/rules/comments/scan.ts +13 -13
  368. package/src/rules/imports/cycles.ts +44 -0
  369. package/src/rules/imports/fences.ts +27 -0
  370. package/src/rules/imports/resolvable.ts +23 -0
  371. package/src/rules/manifests-sorted.ts +23 -0
  372. package/src/rules/max-lines.ts +6 -6
  373. package/src/rules/suppressions/biome/configs.ts +12 -12
  374. package/src/rules/suppressions/biome/declarations.ts +7 -7
  375. package/src/rules/suppressions/biome/json.ts +3 -3
  376. package/src/rules/suppressions/biome/resolve.ts +3 -3
  377. package/src/rules/suppressions/biome/weakenings.ts +18 -19
  378. package/src/rules/suppressions/biome-overrides.ts +7 -7
  379. package/src/rules/suppressions/directives.ts +12 -12
  380. package/src/rules/suppressions/no-double-cast.ts +4 -4
  381. package/src/rules/suppressions/no-inline.ts +13 -37
  382. package/src/rules/suppressions/stylesheet-comments.ts +4 -4
  383. package/src/rules/syntax.ts +4 -4
  384. package/src/standard-schema.ts +1 -1
  385. package/src/vitest.ts +22 -0
  386. package/tsconfig/base.json +21 -21
  387. package/tsconfig/package.json +2 -2
@@ -0,0 +1,62 @@
1
+ import type { ImportEdge } from "../graph.ts";
2
+ import type { Endpoint } from "../resolve.ts";
3
+ import type { Matcher } from "./match.ts";
4
+
5
+ export interface Reached {
6
+ readonly line: number;
7
+ readonly path: readonly string[];
8
+ readonly target: Endpoint;
9
+ }
10
+
11
+ interface Trail {
12
+ readonly line: number;
13
+ readonly path: readonly string[];
14
+ }
15
+
16
+ export function labelOf(endpoint: Endpoint): string {
17
+ if (endpoint.kind === "file") {
18
+ return endpoint.path;
19
+ }
20
+ const named = endpoint.specifier === endpoint.package || endpoint.specifier.startsWith(`${endpoint.package}/`);
21
+ return named ? endpoint.specifier : `${endpoint.specifier} (${endpoint.package})`;
22
+ }
23
+
24
+ export function outgoing(edges: readonly ImportEdge[]): ReadonlyMap<string, readonly ImportEdge[]> {
25
+ const from = new Map<string, ImportEdge[]>();
26
+ for (const edge of edges) {
27
+ const known = from.get(edge.from) ?? [];
28
+ known.push(edge);
29
+ from.set(edge.from, known);
30
+ }
31
+ return from;
32
+ }
33
+
34
+ interface Walk {
35
+ readonly forbidden: Matcher;
36
+ readonly queue: string[];
37
+ readonly reached: Map<string, Reached>;
38
+ readonly trails: Map<string, Trail>;
39
+ }
40
+
41
+ function follow(walk: Walk, before: Trail, edge: ImportEdge): void {
42
+ const label = labelOf(edge.to);
43
+ const trail = { line: before.path.length === 1 ? edge.line : before.line, path: [...before.path, label] };
44
+ if (walk.forbidden(edge.to)) {
45
+ walk.reached.set(label, walk.reached.get(label) ?? { ...trail, target: edge.to });
46
+ } else if (edge.to.kind === "file" && !walk.trails.has(edge.to.path)) {
47
+ walk.trails.set(edge.to.path, trail);
48
+ walk.queue.push(edge.to.path);
49
+ }
50
+ }
51
+
52
+ // A breadth-first walk that stops at each forbidden endpoint, so a finding names the shortest path to the first forbidden step.
53
+ export function reachedFrom(start: string, edgesFrom: ReadonlyMap<string, readonly ImportEdge[]>, forbidden: Matcher): readonly Reached[] {
54
+ const walk: Walk = { forbidden, queue: [start], reached: new Map(), trails: new Map([[start, { line: 0, path: [start] }]]) };
55
+ for (const node of walk.queue) {
56
+ const before = walk.trails.get(node) ?? { line: 0, path: [node] };
57
+ for (const edge of edgesFrom.get(node) ?? []) {
58
+ follow(walk, before, edge);
59
+ }
60
+ }
61
+ return [...walk.reached.values()];
62
+ }
@@ -0,0 +1,37 @@
1
+ import type { External, Selector, Target } from "./model.ts";
2
+
3
+ function target(selector: Selector): Target {
4
+ return { except: (...excluded) => target({ base: selector, excluded: excluded.map((member) => member.selector), kind: "except" }), selector };
5
+ }
6
+
7
+ export function packages(...names: readonly string[]): Target {
8
+ return target({ kind: "packages", names });
9
+ }
10
+
11
+ export function folders(...paths: readonly string[]): Target {
12
+ return target({ kind: "folders", paths });
13
+ }
14
+
15
+ export function files(...paths: readonly string[]): Target {
16
+ return target({ kind: "files", paths });
17
+ }
18
+
19
+ export function modules(...names: readonly string[]): Target {
20
+ return target({ kind: "modules", names });
21
+ }
22
+
23
+ export function scopes(...names: readonly string[]): Target {
24
+ return target({ kind: "scopes", names });
25
+ }
26
+
27
+ export function anyOf(...members: readonly Target[]): Target {
28
+ return target({ kind: "anyOf", members: members.map((member) => member.selector) });
29
+ }
30
+
31
+ export const workspace: Target = target({ kind: "workspace" });
32
+
33
+ export const anything: Target = target({ kind: "anything" });
34
+
35
+ export function external(specifier: string): External {
36
+ return { external: specifier };
37
+ }
@@ -0,0 +1,51 @@
1
+ import type { Endpoint } from "../resolve.ts";
2
+ import { type Compiler, holdsFiles, packageNamed, trimmed } from "./match.ts";
3
+ import type { Selector } from "./model.ts";
4
+
5
+ export interface Vocabulary {
6
+ readonly folder: string;
7
+ readonly subjectOf: (endpoint: Endpoint) => string | undefined;
8
+ }
9
+
10
+ const SOURCE_FOLDER = "src";
11
+
12
+ const MODULE_EXTENSION = /\.[cm]?[jt]sx?$/u;
13
+
14
+ function folderOf(compiler: Compiler, unit: Selector, where: string): string | undefined {
15
+ if (unit.kind === "packages" && unit.names.length === 1) {
16
+ const directory = packageNamed(compiler, unit.names[0] ?? "", where)?.directory;
17
+ return directory === undefined ? undefined : [`${directory}/${SOURCE_FOLDER}`, directory].find((folder) => holdsFiles(compiler, folder));
18
+ }
19
+ if (unit.kind === "folders" && unit.paths.length === 1) {
20
+ const folder = trimmed(unit.paths[0] ?? "");
21
+ if (!holdsFiles(compiler, folder)) {
22
+ compiler.issues.push(`${where}: folders("${folder}") holds no checked file`);
23
+ }
24
+ return folder;
25
+ }
26
+ compiler.issues.push(`${where}: of() takes one package or one folder`);
27
+ return undefined;
28
+ }
29
+
30
+ function subjectAt(folder: string, path: string): string | undefined {
31
+ if (!path.startsWith(`${folder}/`)) {
32
+ return undefined;
33
+ }
34
+ const [first = ""] = path.slice(folder.length + 1).split("/");
35
+ return first.replace(MODULE_EXTENSION, "");
36
+ }
37
+
38
+ export function compileVocabulary(compiler: Compiler, unit: Selector, subjects: readonly string[], where: string): Vocabulary | undefined {
39
+ const folder = folderOf(compiler, unit, where);
40
+ if (folder === undefined) {
41
+ return undefined;
42
+ }
43
+ const known = new Set(compiler.scope.files.flatMap((file) => subjectAt(folder, file) ?? []));
44
+ if (subjects.length === 0) {
45
+ compiler.issues.push(`${where}: mayImportOnly() names no subject`);
46
+ }
47
+ for (const subject of subjects.filter((candidate) => !known.has(candidate))) {
48
+ compiler.issues.push(`${where}: "${subject}" is no module or folder directly in ${folder}`);
49
+ }
50
+ return { folder, subjectOf: (endpoint) => (endpoint.kind === "file" ? subjectAt(folder, endpoint.path) : undefined) };
51
+ }
@@ -0,0 +1,95 @@
1
+ import { execFile } from "node:child_process";
2
+ import { posix } from "node:path";
3
+ import { promisify } from "node:util";
4
+ import { emptyScope, type IgnoreScope, verdictFor, withIgnoreFile } from "../inventory/ignore-scope.ts";
5
+ import type { RuleInputs } from "../rule.ts";
6
+ import type { ImportGraph, UnresolvedImport } from "./graph.ts";
7
+
8
+ type Reader = Pick<RuleInputs, "readText" | "root">;
9
+
10
+ const IGNORE_FILE = ".gitignore";
11
+
12
+ const LEADING_DOT = /^\.\//u;
13
+
14
+ const TRAILING_SLASHES = /\/+$/u;
15
+
16
+ const NODE_MODULES = "node_modules";
17
+
18
+ const NOT_A_REPOSITORY = "not a git repository";
19
+
20
+ const PLACEHOLDERS = new Set([".gitkeep", ".keep"]);
21
+
22
+ const run = promisify(execFile);
23
+
24
+ async function withIgnoreFileIn(reader: Reader, scope: IgnoreScope, directory: string): Promise<IgnoreScope> {
25
+ const contents = await reader.readText(posix.join(directory, IGNORE_FILE));
26
+ return contents === undefined || contents === "" ? scope : withIgnoreFile(scope, posix.join(reader.root, directory), contents);
27
+ }
28
+
29
+ async function ignored(reader: Reader, file: string): Promise<boolean> {
30
+ const parts = file.split("/");
31
+ let scope = emptyScope;
32
+ for (const index of parts.keys()) {
33
+ scope = await withIgnoreFileIn(reader, scope, parts.slice(0, index).join("/") || ".");
34
+ if (verdictFor(scope, posix.join(reader.root, ...parts.slice(0, index + 1)), index < parts.length - 1) === "ignored") {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ async function tracked(root: string, folder: string): Promise<readonly string[]> {
42
+ try {
43
+ const { stdout } = await run("git", ["ls-files", "-z", "--", folder], { cwd: root });
44
+ return stdout.split("\0").filter(Boolean);
45
+ } catch (error) {
46
+ if (error instanceof Error && "stderr" in error && String(error.stderr).includes(NOT_A_REPOSITORY)) {
47
+ return [];
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ function under(folder: string, path: string | undefined): boolean {
54
+ return path?.startsWith(`${folder}/`) === true;
55
+ }
56
+
57
+ async function contentIssue(reader: Reader, folder: string, named: readonly string[]): Promise<string | undefined> {
58
+ const verdicts = await Promise.all(named.map((file) => ignored(reader, file)));
59
+ if (verdicts.includes(false)) {
60
+ return `"${folder}" is not ignored by git, so it holds source, not generated output`;
61
+ }
62
+ const kept = (await tracked(reader.root, folder)).find((file) => !PLACEHOLDERS.has(posix.basename(file)));
63
+ return kept === undefined ? undefined : `"${folder}" holds files git tracks, such as ${kept}, so it is not generated output`;
64
+ }
65
+
66
+ function placeIssue(graph: ImportGraph, folder: string): string | undefined {
67
+ if (folder === "" || folder.startsWith("../") || posix.isAbsolute(folder)) {
68
+ return `"${folder}" is no folder inside the repository`;
69
+ }
70
+ if (folder.split("/").includes(NODE_MODULES)) {
71
+ return `"${folder}" lies in node_modules, which holds installed packages, not generated output`;
72
+ }
73
+ return graph.edges.some((edge) => edge.to.kind === "file" && under(folder, edge.to.path))
74
+ ? undefined
75
+ : `"${folder}" holds no file that an import names`;
76
+ }
77
+
78
+ async function issueOf(reader: Reader, graph: ImportGraph, folder: string): Promise<string | undefined> {
79
+ const misplaced = placeIssue(graph, folder);
80
+ if (misplaced !== undefined) {
81
+ return misplaced;
82
+ }
83
+ const named = [...new Set(graph.edges.flatMap((edge) => (edge.to.kind === "file" && under(folder, edge.to.path) ? [edge.to.path] : [])))];
84
+ return await contentIssue(reader, folder, named);
85
+ }
86
+
87
+ // A generated folder may be empty until its generator runs, so a relative import of a missing file in it resolves.
88
+ export async function withoutGenerated(reader: Reader, graph: ImportGraph, declared: readonly string[]): Promise<readonly UnresolvedImport[]> {
89
+ const folders = declared.map((folder) => posix.normalize(folder).replace(LEADING_DOT, "").replace(TRAILING_SLASHES, ""));
90
+ const issues = (await Promise.all(folders.map((folder) => issueOf(reader, graph, folder)))).flatMap((issue) => issue ?? []);
91
+ if (issues.length > 0) {
92
+ throw new Error(`the generated folders are invalid:\n${issues.map((issue) => ` - ${issue}`).join("\n")}`);
93
+ }
94
+ return graph.unresolved.filter((request) => !folders.some((folder) => under(folder, request.missing)));
95
+ }
@@ -0,0 +1,142 @@
1
+ type Segment = RegExp | typeof GLOBSTAR;
2
+
3
+ const STAR = "*";
4
+
5
+ const GLOBSTAR = "**";
6
+
7
+ const DOT = ".";
8
+
9
+ const NO_DOT = "(?!\\.)";
10
+
11
+ const EXTGLOB = /[?*+@!]\(/u;
12
+
13
+ const BRACE_RANGE = /^[^,]*\.\.[^,]*$/u;
14
+
15
+ const REGEX_SYNTAX = /[\\^$.*+?()[\]{}|/]/u;
16
+
17
+ const CLASS_SYNTAX = /[\\\]^]/gu;
18
+
19
+ const BRACE_DEPTH = new Map([
20
+ ["{", 1],
21
+ ["}", -1],
22
+ ]);
23
+
24
+ function depthChange(char: string | undefined): number {
25
+ return BRACE_DEPTH.get(char ?? "") ?? 0;
26
+ }
27
+
28
+ function charSource(char: string): string {
29
+ if (char === STAR) {
30
+ return "[^/]*";
31
+ }
32
+ if (char === "?") {
33
+ return "[^/]";
34
+ }
35
+ return REGEX_SYNTAX.test(char) ? `\\${char}` : char;
36
+ }
37
+
38
+ function unreadable(pattern: string, problem: string): Error {
39
+ return new Error(`cannot read the workspace pattern "${pattern}": ${problem}`);
40
+ }
41
+
42
+ function closingBrace(text: string, open: number): number {
43
+ let depth = 0;
44
+ for (let index = open; index < text.length; index += 1) {
45
+ depth += depthChange(text[index]);
46
+ if (depth === 0) {
47
+ return index;
48
+ }
49
+ }
50
+ return -1;
51
+ }
52
+
53
+ function choices(body: string): readonly string[] {
54
+ const parts = [""];
55
+ let depth = 0;
56
+ for (const char of body) {
57
+ depth += depthChange(char);
58
+ if (char === "," && depth === 0) {
59
+ parts.push("");
60
+ } else {
61
+ parts[parts.length - 1] += char;
62
+ }
63
+ }
64
+ return parts;
65
+ }
66
+
67
+ function expanded(pattern: string, whole: string): readonly string[] {
68
+ const open = pattern.indexOf("{");
69
+ if (open === -1) {
70
+ return [pattern];
71
+ }
72
+ const close = closingBrace(pattern, open);
73
+ if (close === -1) {
74
+ throw unreadable(whole, "a { never closes");
75
+ }
76
+ const body = pattern.slice(open + 1, close);
77
+ if (BRACE_RANGE.test(body)) {
78
+ throw unreadable(whole, "brace ranges are not supported");
79
+ }
80
+ const options = choices(body);
81
+ if (options.length < 2) {
82
+ throw unreadable(whole, "a { } needs two or more choices");
83
+ }
84
+ return options.flatMap((option) => expanded(`${pattern.slice(0, open)}${option}${pattern.slice(close + 1)}`, whole));
85
+ }
86
+
87
+ function characterClass(segment: string, start: number, whole: string): { readonly end: number; readonly source: string } {
88
+ const negated = segment[start + 1] === "!" || segment[start + 1] === "^";
89
+ const first = start + (negated ? 2 : 1);
90
+ const end = segment.indexOf("]", segment[first] === "]" ? first + 1 : first);
91
+ if (end === -1) {
92
+ throw unreadable(whole, "a [ never closes");
93
+ }
94
+ const body = segment.slice(first, end);
95
+ if (body.includes("[:")) {
96
+ throw unreadable(whole, "character class names are not supported");
97
+ }
98
+ return { end, source: `[${negated ? "^" : ""}${body.replace(CLASS_SYNTAX, "\\$&")}]` };
99
+ }
100
+
101
+ function segmentOf(segment: string, whole: string): Segment {
102
+ if (segment === GLOBSTAR) {
103
+ return GLOBSTAR;
104
+ }
105
+ if (EXTGLOB.test(segment)) {
106
+ throw unreadable(whole, "extended globs are not supported");
107
+ }
108
+ let source = "";
109
+ for (let index = 0; index < segment.length; index += 1) {
110
+ const char = segment[index] ?? "";
111
+ if (char === "[") {
112
+ const found = characterClass(segment, index, whole);
113
+ source += found.source;
114
+ index = found.end;
115
+ } else {
116
+ source += charSource(char);
117
+ }
118
+ }
119
+ return new RegExp(`^${segment.startsWith(DOT) ? "" : NO_DOT}${source}$`, "u");
120
+ }
121
+
122
+ function segmentsMatch(pattern: readonly Segment[], path: readonly string[]): boolean {
123
+ const [first, ...rest] = pattern;
124
+ if (first === undefined) {
125
+ return path.length === 0;
126
+ }
127
+ if (first === GLOBSTAR) {
128
+ const hidden = path.findIndex((segment) => segment.startsWith(DOT));
129
+ const reachable = hidden === -1 ? path.length : hidden;
130
+ return [...path.keys(), path.length].some((skipped) => skipped <= reachable && segmentsMatch(rest, path.slice(skipped)));
131
+ }
132
+ const [head, ...tail] = path;
133
+ return head !== undefined && first.test(head) && segmentsMatch(rest, tail);
134
+ }
135
+
136
+ export function globMatcher(pattern: string): (path: string) => boolean {
137
+ if (pattern.includes("\\")) {
138
+ throw unreadable(pattern, "escapes are not supported");
139
+ }
140
+ const alternatives = expanded(pattern, pattern).map((alternative) => alternative.split("/").map((segment) => segmentOf(segment, pattern)));
141
+ return (path) => alternatives.some((segments) => segmentsMatch(segments, path.split("/")));
142
+ }
@@ -0,0 +1,81 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { join, posix } from "node:path";
3
+ import type { RuleInputs, SourceFile } from "../rule.ts";
4
+ import { parse } from "../rules/syntax.ts";
5
+ import { ambientModules } from "./ambient.ts";
6
+ import { type ImportKind, type ImportRequest, importsOf } from "./extract.ts";
7
+ import { projectsFor } from "./projects.ts";
8
+ import { type Endpoint, isDeclarationFile, resolveImport } from "./resolve.ts";
9
+ import { type WorkspacePackage, workspacePackages } from "./workspace.ts";
10
+
11
+ export interface ImportEdge {
12
+ readonly from: string;
13
+ readonly kind: ImportKind;
14
+ readonly line: number;
15
+ readonly specifier: string;
16
+ readonly to: Endpoint;
17
+ readonly type: boolean;
18
+ }
19
+
20
+ export type UnresolvedImport = Omit<ImportEdge, "to"> & { readonly missing: string | undefined };
21
+
22
+ export interface ImportGraph {
23
+ readonly edges: readonly ImportEdge[];
24
+ readonly modules: readonly string[];
25
+ readonly packages: readonly WorkspacePackage[];
26
+ readonly unresolved: readonly UnresolvedImport[];
27
+ }
28
+
29
+ const QUERY = /\?.*$/u;
30
+
31
+ const NO_MODULES =
32
+ "the import graph covers no modules: the sources hold no TypeScript or JavaScript module. Point `sources` at the code, or turn the imports rules off.";
33
+
34
+ // A relative import of a missing file still names a path, so fences see it before its generator writes it.
35
+ function missingTarget(from: string, request: ImportRequest): string | undefined {
36
+ const relative = request.kind === "path-reference" || request.specifier.startsWith("./") || request.specifier.startsWith("../");
37
+ const target = posix.join(posix.dirname(from), request.specifier.replace(QUERY, ""));
38
+ return relative && !target.startsWith("../") ? target : undefined;
39
+ }
40
+
41
+ function walk(inputs: RuleInputs, root: string): Pick<ImportGraph, "edges" | "modules" | "unresolved"> {
42
+ const projectOf = projectsFor(root);
43
+ const parsed = inputs.sources.flatMap((file) => {
44
+ const syntax = parse(file);
45
+ return syntax === undefined ? [] : [{ file, path: join(root, file.path), syntax }];
46
+ });
47
+ const ambient = ambientModules(parsed);
48
+ const edges: ImportEdge[] = [];
49
+ const unresolved: UnresolvedImport[] = [];
50
+ for (const { file, path, syntax } of parsed) {
51
+ const project = projectOf(path);
52
+ for (const request of importsOf(syntax, isDeclarationFile(file.path))) {
53
+ const to = resolveImport(root, ambient, request, path, project);
54
+ if (to === undefined) {
55
+ const missing = missingTarget(file.path, request);
56
+ unresolved.push({ ...request, from: file.path, missing });
57
+ edges.push(...(missing === undefined ? [] : [{ ...request, from: file.path, to: { kind: "file" as const, path: missing } }]));
58
+ } else {
59
+ edges.push({ ...request, from: file.path, to });
60
+ }
61
+ }
62
+ }
63
+ return { edges, modules: parsed.map(({ file }) => file.path), unresolved };
64
+ }
65
+
66
+ async function build(inputs: RuleInputs): Promise<ImportGraph> {
67
+ const walked = walk(inputs, realpathSync(inputs.root));
68
+ if (walked.modules.length === 0) {
69
+ throw new Error(NO_MODULES);
70
+ }
71
+ return { ...walked, packages: await workspacePackages(inputs) };
72
+ }
73
+
74
+ const graphs = new WeakMap<readonly SourceFile[], Promise<ImportGraph>>();
75
+
76
+ // The three imports rules run on one scan, so they share the graph built from its sources.
77
+ export function importGraph(inputs: RuleInputs): Promise<ImportGraph> {
78
+ const known = graphs.get(inputs.sources) ?? build(inputs);
79
+ graphs.set(inputs.sources, known);
80
+ return known;
81
+ }
@@ -0,0 +1,28 @@
1
+ function trail(previous: ReadonlyMap<string, string | undefined>, node: string): readonly string[] {
2
+ const path: string[] = [];
3
+ for (let step: string | undefined = node; step !== undefined; step = previous.get(step)) {
4
+ path.unshift(step);
5
+ }
6
+ return path;
7
+ }
8
+
9
+ export function shortestPath(
10
+ start: string,
11
+ neighbours: (node: string) => readonly string[],
12
+ goal: (node: string) => boolean,
13
+ ): readonly string[] | undefined {
14
+ const previous = new Map<string, string | undefined>([[start, undefined]]);
15
+ const queue = [start];
16
+ for (const node of queue) {
17
+ for (const next of neighbours(node)) {
18
+ if (goal(next)) {
19
+ return [...trail(previous, node), next];
20
+ }
21
+ if (!previous.has(next)) {
22
+ previous.set(next, node);
23
+ queue.push(next);
24
+ }
25
+ }
26
+ }
27
+ return undefined;
28
+ }
@@ -0,0 +1,134 @@
1
+ const KEY = /^(?:packages|"packages"|'packages')\s*:/u;
2
+
3
+ const ITEM = "-";
4
+
5
+ const SPACE = /\s/u;
6
+
7
+ const LINE_BREAK = /\r?\n/u;
8
+
9
+ function failure(problem: string, cause?: unknown): Error {
10
+ return new Error(`pnpm-workspace.yaml: ${problem}`, { cause });
11
+ }
12
+
13
+ function withoutComment(line: string): string {
14
+ let quote: string | undefined;
15
+ for (let index = 0; index < line.length; index += 1) {
16
+ const char = line[index];
17
+ if (quote !== undefined) {
18
+ quote = char === quote ? undefined : quote;
19
+ } else if (char === '"' || char === "'") {
20
+ quote = char;
21
+ } else if (char === "#" && (index === 0 || SPACE.test(line[index - 1] ?? ""))) {
22
+ return line.slice(0, index).trimEnd();
23
+ }
24
+ }
25
+ return line.trimEnd();
26
+ }
27
+
28
+ function scalar(raw: string): string {
29
+ const value = raw.trim();
30
+ if (value.startsWith('"') && value.endsWith('"') && value.length > 1) {
31
+ try {
32
+ return String(JSON.parse(value));
33
+ } catch (error) {
34
+ throw failure(`cannot read "${value}" in packages`, error);
35
+ }
36
+ }
37
+ if (value.startsWith("'") && value.endsWith("'") && value.length > 1) {
38
+ return value.slice(1, -1).replaceAll("''", "'");
39
+ }
40
+ if (value === "" || value.includes(": ") || value.endsWith(":") || ["[", "{", "'", '"'].some((opening) => value.startsWith(opening))) {
41
+ throw failure(`cannot read "${value}" in packages`);
42
+ }
43
+ return value;
44
+ }
45
+
46
+ const FLOW_INDICATOR = /[{}[\]]/u;
47
+
48
+ interface FlowScan {
49
+ current: string;
50
+ readonly entries: string[];
51
+ quote: string | undefined;
52
+ }
53
+
54
+ function flowScalar(raw: string): string {
55
+ const value = raw.trim();
56
+ const quoted = value.startsWith('"') || value.startsWith("'");
57
+ if (!quoted && FLOW_INDICATOR.test(value)) {
58
+ throw failure(`cannot read "${value}" in packages; quote a pattern that holds { } [ ] or ,`);
59
+ }
60
+ return scalar(value);
61
+ }
62
+
63
+ function closed(scan: FlowScan, rest: string): readonly string[] {
64
+ if (rest.trim() !== "") {
65
+ throw failure(`cannot read "${rest.trim()}" after the packages list`);
66
+ }
67
+ return [...scan.entries, scan.current].filter((entry) => entry.trim() !== "").map(flowScalar);
68
+ }
69
+
70
+ function scanned(scan: FlowScan, char: string): "close" | undefined {
71
+ if (scan.quote !== undefined) {
72
+ scan.quote = char === scan.quote ? undefined : scan.quote;
73
+ } else if (char === "]") {
74
+ return "close";
75
+ } else if (char === ",") {
76
+ scan.entries.push(scan.current);
77
+ scan.current = "";
78
+ return undefined;
79
+ } else if (char === '"' || char === "'") {
80
+ scan.quote = char;
81
+ }
82
+ scan.current += char;
83
+ return undefined;
84
+ }
85
+
86
+ function flowList(lines: readonly string[]): readonly string[] {
87
+ const scan: FlowScan = { current: "", entries: [], quote: undefined };
88
+ for (const [number, line] of lines.map(withoutComment).entries()) {
89
+ for (let index = number === 0 ? 1 : 0; index < line.length; index += 1) {
90
+ if (scanned(scan, line[index] ?? "") === "close") {
91
+ return closed(scan, line.slice(index + 1));
92
+ }
93
+ }
94
+ scan.current += " ";
95
+ }
96
+ throw failure("the packages list never closes");
97
+ }
98
+
99
+ function blockList(lines: readonly string[]): readonly string[] {
100
+ const patterns: string[] = [];
101
+ for (const line of lines.map(withoutComment)) {
102
+ const trimmed = line.trim();
103
+ if (trimmed === "") {
104
+ continue;
105
+ }
106
+ const item = trimmed === ITEM || trimmed.startsWith(`${ITEM} `);
107
+ if (!(item || line.startsWith(" ") || line.startsWith("\t"))) {
108
+ break;
109
+ }
110
+ if (!item) {
111
+ throw failure(`cannot read "${trimmed}" in packages`);
112
+ }
113
+ patterns.push(scalar(trimmed.slice(ITEM.length)));
114
+ }
115
+ return patterns;
116
+ }
117
+
118
+ export function pnpmWorkspacePatterns(text: string): readonly string[] {
119
+ const lines = text.split(LINE_BREAK);
120
+ const start = lines.findIndex((line) => KEY.test(line));
121
+ if (start === -1) {
122
+ return [];
123
+ }
124
+ const rest = withoutComment(lines[start] ?? "")
125
+ .replace(KEY, "")
126
+ .trim();
127
+ if (rest.startsWith("[")) {
128
+ return flowList([rest, ...lines.slice(start + 1)]);
129
+ }
130
+ if (rest !== "") {
131
+ throw failure(`cannot read "${rest}" in packages`);
132
+ }
133
+ return blockList(lines.slice(start + 1));
134
+ }