@proteinjs/reflection-build 2.0.2 → 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 (246) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/src/ReflectionDoctor.d.ts +6 -1
  3. package/dist/src/ReflectionDoctor.js +53 -51
  4. package/dist/src/ReflectionDoctor.js.map +1 -1
  5. package/dist/src/codegen/CanonicalSourceGraph.d.ts +22 -0
  6. package/dist/src/codegen/CanonicalSourceGraph.js +60 -0
  7. package/dist/src/codegen/CanonicalSourceGraph.js.map +1 -0
  8. package/dist/src/codegen/writeGeneratedIndex.d.ts +12 -3
  9. package/dist/src/codegen/writeGeneratedIndex.js +21 -13
  10. package/dist/src/codegen/writeGeneratedIndex.js.map +1 -1
  11. package/dist/src/parser/PackageSourceFiles.d.ts +30 -0
  12. package/dist/src/parser/PackageSourceFiles.js +151 -0
  13. package/dist/src/parser/PackageSourceFiles.js.map +1 -0
  14. package/dist/src/parser/SharedQualifiedNames.d.ts +43 -0
  15. package/dist/src/parser/SharedQualifiedNames.js +81 -0
  16. package/dist/src/parser/SharedQualifiedNames.js.map +1 -0
  17. package/dist/src/parser/createGraphBuilder.d.ts +6 -1
  18. package/dist/src/parser/createGraphBuilder.js +16 -31
  19. package/dist/src/parser/createGraphBuilder.js.map +1 -1
  20. package/dist/src/parser/createSourceGraph.d.ts +6 -1
  21. package/dist/src/parser/createSourceGraph.js +23 -38
  22. package/dist/src/parser/createSourceGraph.js.map +1 -1
  23. package/dist/test/Class.test.js +1 -1
  24. package/dist/test/Class.test.js.map +1 -1
  25. package/dist/test/ExamplePackageFixture.d.ts +47 -0
  26. package/dist/test/ExamplePackageFixture.js +207 -0
  27. package/dist/test/ExamplePackageFixture.js.map +1 -0
  28. package/dist/test/GraphEmitRelativePaths.test.js +1 -1
  29. package/dist/test/GraphEmitRelativePaths.test.js.map +1 -1
  30. package/dist/test/PackageSourceFiles.test.d.ts +1 -0
  31. package/dist/test/PackageSourceFiles.test.js +57 -0
  32. package/dist/test/PackageSourceFiles.test.js.map +1 -0
  33. package/dist/test/ReflectionDoctor.test.js +55 -18
  34. package/dist/test/ReflectionDoctor.test.js.map +1 -1
  35. package/dist/test/ReproducibleGeneratedIndex.test.d.ts +1 -0
  36. package/dist/test/ReproducibleGeneratedIndex.test.js +224 -0
  37. package/dist/test/ReproducibleGeneratedIndex.test.js.map +1 -0
  38. package/dist/test/SharedQualifiedNames.test.d.ts +1 -0
  39. package/dist/test/SharedQualifiedNames.test.js +186 -0
  40. package/dist/test/SharedQualifiedNames.test.js.map +1 -0
  41. package/dist/test/examples/reproducible/index.d.ts +4 -0
  42. package/dist/test/examples/reproducible/index.js +21 -0
  43. package/dist/test/examples/reproducible/index.js.map +1 -0
  44. package/dist/test/examples/reproducible/src/alpha/Alpha.d.ts +10 -0
  45. package/dist/test/examples/reproducible/src/alpha/Alpha.js +18 -0
  46. package/dist/test/examples/reproducible/src/alpha/Alpha.js.map +1 -0
  47. package/dist/test/examples/reproducible/src/alpha/Defaults.d.ts +2 -0
  48. package/dist/test/examples/reproducible/src/alpha/Defaults.js +6 -0
  49. package/dist/test/examples/reproducible/src/alpha/Defaults.js.map +1 -0
  50. package/dist/test/examples/reproducible/src/beta/Beta.d.ts +5 -0
  51. package/dist/test/examples/reproducible/src/beta/Beta.js +12 -0
  52. package/dist/test/examples/reproducible/src/beta/Beta.js.map +1 -0
  53. package/dist/test/examples/reproducible/src/beta/deep/Gamma.d.ts +4 -0
  54. package/dist/test/examples/reproducible/src/beta/deep/Gamma.js +11 -0
  55. package/dist/test/examples/reproducible/src/beta/deep/Gamma.js.map +1 -0
  56. package/dist/test/examples/reproducible/src/zeta/Defaults.d.ts +2 -0
  57. package/dist/test/examples/reproducible/src/zeta/Defaults.js +6 -0
  58. package/dist/test/examples/reproducible/src/zeta/Defaults.js.map +1 -0
  59. package/dist/test/examples/reproducible/src/zeta/Zeta.d.ts +7 -0
  60. package/dist/test/examples/reproducible/src/zeta/Zeta.js +18 -0
  61. package/dist/test/examples/reproducible/src/zeta/Zeta.js.map +1 -0
  62. package/dist/test/examples/shared-name/index.d.ts +1 -0
  63. package/dist/test/examples/shared-name/index.js +18 -0
  64. package/dist/test/examples/shared-name/index.js.map +1 -0
  65. package/dist/test/examples/shared-name/src/alpha/Widget.d.ts +4 -0
  66. package/dist/test/examples/shared-name/src/alpha/Widget.js +3 -0
  67. package/dist/test/examples/shared-name/src/alpha/Widget.js.map +1 -0
  68. package/dist/test/examples/shared-name/src/beta/Widget.d.ts +3 -0
  69. package/dist/test/examples/shared-name/src/beta/Widget.js +13 -0
  70. package/dist/test/examples/shared-name/src/beta/Widget.js.map +1 -0
  71. package/dist/test/examples/shared-name/src/gamma/Gear.d.ts +7 -0
  72. package/dist/test/examples/shared-name/src/gamma/Gear.js +5 -0
  73. package/dist/test/examples/shared-name/src/gamma/Gear.js.map +1 -0
  74. package/dist/test/examples/shared-name-pruned/index.d.ts +1 -0
  75. package/dist/test/examples/shared-name-pruned/index.js +18 -0
  76. package/dist/test/examples/shared-name-pruned/index.js.map +1 -0
  77. package/dist/test/examples/shared-name-pruned/src/alpha/Limits.d.ts +3 -0
  78. package/dist/test/examples/shared-name-pruned/src/alpha/Limits.js +6 -0
  79. package/dist/test/examples/shared-name-pruned/src/alpha/Limits.js.map +1 -0
  80. package/dist/test/examples/shared-name-pruned/src/alpha/Shape.d.ts +3 -0
  81. package/dist/test/examples/shared-name-pruned/src/alpha/Shape.js +3 -0
  82. package/dist/test/examples/shared-name-pruned/src/alpha/Shape.js.map +1 -0
  83. package/dist/test/examples/shared-name-pruned/src/alpha/Tile.d.ts +7 -0
  84. package/dist/test/examples/shared-name-pruned/src/alpha/Tile.js +11 -0
  85. package/dist/test/examples/shared-name-pruned/src/alpha/Tile.js.map +1 -0
  86. package/dist/test/examples/shared-name-pruned/src/beta/Limits.d.ts +3 -0
  87. package/dist/test/examples/shared-name-pruned/src/beta/Limits.js +6 -0
  88. package/dist/test/examples/shared-name-pruned/src/beta/Limits.js.map +1 -0
  89. package/dist/test/examples/shared-name-pruned/src/beta/Shape.d.ts +3 -0
  90. package/dist/test/examples/shared-name-pruned/src/beta/Shape.js +3 -0
  91. package/dist/test/examples/shared-name-pruned/src/beta/Shape.js.map +1 -0
  92. package/dist/test/examples/source-repository/a/generated/index.js +28 -28
  93. package/dist/test/examples/source-repository/a/generated/index.js.map +1 -1
  94. package/dist/test/examples/source-repository/a/src/ExtendsForeignType.d.ts +1 -1
  95. package/dist/test/examples/source-repository/a/src/ExtendsForeignType.js +5 -5
  96. package/dist/test/examples/source-repository/a/src/ExtendsForeignType.js.map +1 -1
  97. package/dist/test/examples/source-repository/b/generated/index.js +3 -3
  98. package/dist/test/examples/source-repository/b/generated/index.js.map +1 -1
  99. package/package.json +5 -5
  100. package/src/ReflectionDoctor.ts +37 -17
  101. package/src/codegen/CanonicalSourceGraph.ts +50 -0
  102. package/src/codegen/writeGeneratedIndex.ts +35 -9
  103. package/src/parser/PackageSourceFiles.ts +66 -0
  104. package/src/parser/SharedQualifiedNames.ts +76 -0
  105. package/src/parser/createGraphBuilder.ts +25 -10
  106. package/src/parser/createSourceGraph.ts +14 -16
  107. package/test/Class.test.ts +1 -1
  108. package/test/ExamplePackageFixture.ts +142 -0
  109. package/test/GraphEmitRelativePaths.test.ts +1 -1
  110. package/test/PackageSourceFiles.test.ts +42 -0
  111. package/test/ReflectionDoctor.test.ts +50 -12
  112. package/test/ReproducibleGeneratedIndex.test.ts +114 -0
  113. package/test/SharedQualifiedNames.test.ts +89 -0
  114. package/test/examples/reproducible/index.ts +4 -0
  115. package/test/examples/reproducible/src/alpha/Alpha.ts +13 -0
  116. package/test/examples/reproducible/src/alpha/Defaults.ts +4 -0
  117. package/test/examples/reproducible/src/beta/Beta.ts +7 -0
  118. package/test/examples/reproducible/src/beta/deep/Gamma.ts +5 -0
  119. package/test/examples/reproducible/src/zeta/Defaults.ts +4 -0
  120. package/test/examples/reproducible/src/zeta/Zeta.ts +9 -0
  121. package/test/examples/shared-name/index.ts +1 -0
  122. package/test/examples/shared-name/src/alpha/Widget.ts +6 -0
  123. package/test/examples/shared-name/src/beta/Widget.ts +5 -0
  124. package/test/examples/shared-name/src/gamma/Gear.ts +9 -0
  125. package/test/examples/shared-name-pruned/index.ts +1 -0
  126. package/test/examples/shared-name-pruned/src/alpha/Limits.ts +2 -0
  127. package/test/examples/shared-name-pruned/src/alpha/Shape.ts +3 -0
  128. package/test/examples/shared-name-pruned/src/alpha/Tile.ts +9 -0
  129. package/test/examples/shared-name-pruned/src/beta/Limits.ts +2 -0
  130. package/test/examples/shared-name-pruned/src/beta/Shape.ts +4 -0
  131. package/test/examples/source-repository/a/dist/codegen/CanonicalSourceGraph.d.ts +22 -0
  132. package/test/examples/source-repository/a/dist/codegen/CanonicalSourceGraph.js +60 -0
  133. package/test/examples/source-repository/a/dist/codegen/CanonicalSourceGraph.js.map +1 -0
  134. package/test/examples/source-repository/a/dist/codegen/writeGeneratedIndex.d.ts +12 -3
  135. package/test/examples/source-repository/a/dist/codegen/writeGeneratedIndex.js +21 -13
  136. package/test/examples/source-repository/a/dist/codegen/writeGeneratedIndex.js.map +1 -1
  137. package/test/examples/source-repository/a/dist/generated/index.js +28 -28
  138. package/test/examples/source-repository/a/dist/generated/index.js.map +1 -1
  139. package/test/examples/source-repository/a/dist/parser/PackageSourceFiles.d.ts +30 -0
  140. package/test/examples/source-repository/a/dist/parser/PackageSourceFiles.js +151 -0
  141. package/test/examples/source-repository/a/dist/parser/PackageSourceFiles.js.map +1 -0
  142. package/test/examples/source-repository/a/dist/parser/SharedQualifiedNames.d.ts +43 -0
  143. package/test/examples/source-repository/a/dist/parser/SharedQualifiedNames.js +81 -0
  144. package/test/examples/source-repository/a/dist/parser/SharedQualifiedNames.js.map +1 -0
  145. package/test/examples/source-repository/a/dist/parser/createGraphBuilder.d.ts +6 -1
  146. package/test/examples/source-repository/a/dist/parser/createGraphBuilder.js +16 -31
  147. package/test/examples/source-repository/a/dist/parser/createGraphBuilder.js.map +1 -1
  148. package/test/examples/source-repository/a/dist/parser/createSourceGraph.d.ts +6 -1
  149. package/test/examples/source-repository/a/dist/parser/createSourceGraph.js +23 -38
  150. package/test/examples/source-repository/a/dist/parser/createSourceGraph.js.map +1 -1
  151. package/test/examples/source-repository/a/dist/src/ExtendsForeignType.d.ts +1 -1
  152. package/test/examples/source-repository/a/dist/src/ExtendsForeignType.js +5 -5
  153. package/test/examples/source-repository/a/dist/src/ExtendsForeignType.js.map +1 -1
  154. package/test/examples/source-repository/a/generated/index.ts +44 -44
  155. package/test/examples/source-repository/a/node_modules/.package-lock.json +13 -13
  156. package/test/examples/source-repository/a/node_modules/@proteinjs/util/CHANGELOG.md +8 -0
  157. package/test/examples/source-repository/a/node_modules/@proteinjs/util/LICENSE +21 -0
  158. package/test/examples/source-repository/a/node_modules/@proteinjs/util/package.json +3 -2
  159. package/test/examples/source-repository/a/node_modules/@proteinjs/util/tsconfig.json +1 -0
  160. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/CHANGELOG.md +63 -0
  161. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/LICENSE +21 -0
  162. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/index.d.ts +1 -0
  163. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/index.js +1 -0
  164. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/index.js.map +1 -1
  165. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/ArgsMap.js +7 -4
  166. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/ArgsMap.js.map +1 -1
  167. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/Fs.js +16 -12
  168. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/Fs.js.map +1 -1
  169. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.d.ts +59 -13
  170. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js +119 -80
  171. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js.map +1 -1
  172. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/WorkspaceDeclaration.d.ts +91 -0
  173. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/WorkspaceDeclaration.js +608 -0
  174. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/WorkspaceDeclaration.js.map +1 -0
  175. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/dist/src/promisifiedFs.d.ts +0 -1
  176. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/index.ts +1 -0
  177. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/package.json +4 -3
  178. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/ArgsMap.ts +7 -4
  179. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/Fs.ts +12 -9
  180. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/PackageUtil.ts +92 -38
  181. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/src/WorkspaceDeclaration.ts +340 -0
  182. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/ArgsMap.test.ts +35 -0
  183. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/Fs.getFilePathsMatchingGlob.test.ts +70 -0
  184. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/PackageUtil.getLocalPackageMap.test.ts +107 -0
  185. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/PackageUtil.pathKeyedIdentity.test.ts +169 -0
  186. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/test/WorkspaceDeclaration.test.ts +174 -0
  187. package/test/examples/source-repository/a/node_modules/@proteinjs/util-node/tsconfig.json +1 -0
  188. package/test/examples/source-repository/a/package-lock.json +16 -16
  189. package/test/examples/source-repository/a/package.json +3 -3
  190. package/test/examples/source-repository/a/src/ExtendsForeignType.ts +1 -1
  191. package/test/examples/source-repository/b/dist/codegen/CanonicalSourceGraph.d.ts +22 -0
  192. package/test/examples/source-repository/b/dist/codegen/CanonicalSourceGraph.js +60 -0
  193. package/test/examples/source-repository/b/dist/codegen/CanonicalSourceGraph.js.map +1 -0
  194. package/test/examples/source-repository/b/dist/codegen/writeGeneratedIndex.d.ts +12 -3
  195. package/test/examples/source-repository/b/dist/codegen/writeGeneratedIndex.js +21 -13
  196. package/test/examples/source-repository/b/dist/codegen/writeGeneratedIndex.js.map +1 -1
  197. package/test/examples/source-repository/b/dist/generated/index.js +3 -3
  198. package/test/examples/source-repository/b/dist/generated/index.js.map +1 -1
  199. package/test/examples/source-repository/b/dist/parser/PackageSourceFiles.d.ts +30 -0
  200. package/test/examples/source-repository/b/dist/parser/PackageSourceFiles.js +151 -0
  201. package/test/examples/source-repository/b/dist/parser/PackageSourceFiles.js.map +1 -0
  202. package/test/examples/source-repository/b/dist/parser/SharedQualifiedNames.d.ts +43 -0
  203. package/test/examples/source-repository/b/dist/parser/SharedQualifiedNames.js +81 -0
  204. package/test/examples/source-repository/b/dist/parser/SharedQualifiedNames.js.map +1 -0
  205. package/test/examples/source-repository/b/dist/parser/createGraphBuilder.d.ts +6 -1
  206. package/test/examples/source-repository/b/dist/parser/createGraphBuilder.js +16 -31
  207. package/test/examples/source-repository/b/dist/parser/createGraphBuilder.js.map +1 -1
  208. package/test/examples/source-repository/b/dist/parser/createSourceGraph.d.ts +6 -1
  209. package/test/examples/source-repository/b/dist/parser/createSourceGraph.js +23 -38
  210. package/test/examples/source-repository/b/dist/parser/createSourceGraph.js.map +1 -1
  211. package/test/examples/source-repository/b/generated/index.ts +6 -6
  212. package/test/examples/source-repository/b/node_modules/.package-lock.json +10 -10
  213. package/test/examples/source-repository/b/node_modules/@proteinjs/util/CHANGELOG.md +8 -0
  214. package/test/examples/source-repository/b/node_modules/@proteinjs/util/LICENSE +21 -0
  215. package/test/examples/source-repository/b/node_modules/@proteinjs/util/package.json +3 -2
  216. package/test/examples/source-repository/b/node_modules/@proteinjs/util/tsconfig.json +1 -0
  217. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/CHANGELOG.md +63 -0
  218. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/LICENSE +21 -0
  219. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/index.d.ts +1 -0
  220. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/index.js +1 -0
  221. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/index.js.map +1 -1
  222. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/ArgsMap.js +7 -4
  223. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/ArgsMap.js.map +1 -1
  224. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/Fs.js +16 -12
  225. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/Fs.js.map +1 -1
  226. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.d.ts +59 -13
  227. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js +119 -80
  228. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/PackageUtil.js.map +1 -1
  229. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/WorkspaceDeclaration.d.ts +91 -0
  230. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/WorkspaceDeclaration.js +608 -0
  231. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/WorkspaceDeclaration.js.map +1 -0
  232. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/dist/src/promisifiedFs.d.ts +0 -1
  233. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/index.ts +1 -0
  234. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/package.json +4 -3
  235. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/src/ArgsMap.ts +7 -4
  236. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/src/Fs.ts +12 -9
  237. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/src/PackageUtil.ts +92 -38
  238. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/src/WorkspaceDeclaration.ts +340 -0
  239. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/ArgsMap.test.ts +35 -0
  240. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/Fs.getFilePathsMatchingGlob.test.ts +70 -0
  241. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/PackageUtil.getLocalPackageMap.test.ts +107 -0
  242. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/PackageUtil.pathKeyedIdentity.test.ts +169 -0
  243. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/test/WorkspaceDeclaration.test.ts +174 -0
  244. package/test/examples/source-repository/b/node_modules/@proteinjs/util-node/tsconfig.json +1 -0
  245. package/test/examples/source-repository/b/package-lock.json +13 -13
  246. package/test/examples/source-repository/b/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/util-node",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "Util libs for node",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@dagrejs/graphlib": "2.1.4",
28
- "@proteinjs/util": "^1.6.1",
28
+ "@proteinjs/util": "^1.6.2",
29
29
  "fs-extra": "11.1.1",
30
30
  "globby": "11.0.1",
31
31
  "typescript": "5.2.2"
@@ -42,5 +42,6 @@
42
42
  "eslint-plugin-prettier": "5.1.3",
43
43
  "jest": "^29.6.4",
44
44
  "ts-jest": "^29.1.1"
45
- }
45
+ },
46
+ "gitHead": "6ddfa98b5143e30991703881a29d1a73cec35e85"
46
47
  }
@@ -9,11 +9,14 @@ export const parseArgsMap = (rawArgs: string[]): ArgsMap => {
9
9
  return;
10
10
  }
11
11
 
12
- const keyValue = arg.slice(2).split('=');
13
- if (keyValue.length > 1) {
14
- args[keyValue[0]] = keyValue[1];
12
+ // Split on the FIRST '=' only — values legitimately contain '=' (e.g. --note=port=9040)
13
+ // and must be preserved in full.
14
+ const keyValue = arg.slice(2);
15
+ const separatorIndex = keyValue.indexOf('=');
16
+ if (separatorIndex >= 0) {
17
+ args[keyValue.slice(0, separatorIndex)] = keyValue.slice(separatorIndex + 1);
15
18
  } else {
16
- args[keyValue[0]] = true;
19
+ args[keyValue] = true;
17
20
  }
18
21
  });
19
22
  return args;
@@ -104,21 +104,24 @@ export class Fs {
104
104
  // @param globIgnorePatterns ie. ['**/node_modules/**', '**/dist/**'] to ignore these directories
105
105
  // @return string[] of file paths
106
106
  static async getFilePaths(dir: string, globIgnorePatterns: string[] = []) {
107
- return await globby(dir + '**/*', {
108
- ignore: [...globIgnorePatterns],
109
- });
107
+ return await Fs.getFilePathsMatchingGlob(dir, '**/*', globIgnorePatterns);
110
108
  }
111
109
 
112
110
  // @param dirPrefix recursively search for files in this dir
113
111
  // @param glob file matching pattern ie. **/package.json
114
112
  // @param globIgnorePatterns ie. ['**/node_modules/**', '**/dist/**'] to ignore these directories
115
- // @return string[] of file paths
113
+ // @return string[] of absolute file paths
116
114
  static async getFilePathsMatchingGlob(dirPrefix: string, glob: string, globIgnorePatterns: string[] = []) {
117
- if (dirPrefix[dirPrefix.length - 1] != path.sep) {
118
- dirPrefix += path.sep;
119
- }
120
-
121
- return await globby(dirPrefix + glob, {
115
+ // Match relative to the directory (`cwd`) rather than folding it into an absolute pattern.
116
+ // In the absolute form fast-glob matched the ignore list against the absolute entry path,
117
+ // and a globstar does not cross a dot-segment — so under any base path containing a
118
+ // dot-directory (`~/.n3xa/workspaces/<name>`, a `.scratch` estate) the ignore list matched
119
+ // nothing: every node_modules/dist was walked in full, workspace symlinks followed into
120
+ // cycles, and the caller ran out of heap on a large tree. Relative matching keeps the ignore
121
+ // list and the entry paths in the same frame, so the prune holds under any base path.
122
+ return await globby(glob, {
123
+ cwd: dirPrefix,
124
+ absolute: true,
122
125
  ignore: [...globIgnorePatterns],
123
126
  });
124
127
  }
@@ -4,6 +4,7 @@ import * as fs from 'fs/promises';
4
4
  import { Graph, GraphAlgorithms } from '@proteinjs/util';
5
5
  import { cmd } from './cmd';
6
6
  import { Fs } from './Fs';
7
+ import { WorkspaceDeclaration } from './WorkspaceDeclaration';
7
8
 
8
9
  export type Package = {
9
10
  name: string;
@@ -23,12 +24,31 @@ export type LocalPackage = {
23
24
  };
24
25
  };
25
26
 
27
+ /**
28
+ * Name-keyed resolution index: dependency declarations reference packages BY NAME, so name →
29
+ * package is how a declared dep resolves to a workspace member. Names are not identities —
30
+ * every workspace root in a metarepo is named `root` — so when several packages share a name,
31
+ * this map holds only one of them (deterministically: the lexicographically last package.json
32
+ * path wins). Enumeration and identity questions belong to `LocalPackagePathMap`.
33
+ */
26
34
  export type LocalPackageMap = {
27
35
  [packageName: string]: LocalPackage;
28
36
  };
29
37
 
38
+ /**
39
+ * Path-keyed identity map: one entry per discovered package.json, keyed by its absolute path.
40
+ * This is the DISCOVERY truth — same-named packages (the metarepo root, the app root, and every
41
+ * nested lerna root are all named `root`) never collide here. Use it wherever the question is
42
+ * "which packages exist" (enumeration, symlink coverage); use `LocalPackageMap` only to resolve
43
+ * a dependency NAME to its workspace member.
44
+ */
45
+ export type LocalPackagePathMap = {
46
+ [packageJsonPath: string]: LocalPackage;
47
+ };
48
+
30
49
  export type WorkspaceMetadata = {
31
50
  packageMap: LocalPackageMap;
51
+ packagePathMap: LocalPackagePathMap; // every discovered package, path-keyed (collision-free)
32
52
  packageGraph: any; // @dagrejs/graphlib.Graph
33
53
  sortedPackageNames: string[]; // local package names, in dependency order (ie. if a depends on b, [b, a] will be returned)
34
54
  workspaceToPackageMap: { [workspacePath: string]: string[] }; // string[] is names of packages in workspace
@@ -155,23 +175,40 @@ export class PackageUtil {
155
175
  }
156
176
 
157
177
  /**
158
- * Get map of local packages within repo specified by directory path
178
+ * Get the name-keyed resolution index of local packages within the repo specified by
179
+ * directory path. Derived from `getLocalPackagePathMap` (which owns discovery); see
180
+ * `LocalPackageMap` for the name-collision semantics.
159
181
  *
160
182
  * @param dir dir path that contains local packages
161
- * @param globIgnorePatterns already includes: ['**\/node_modules/**', '**\/dist/**']
162
183
  * @returns {[packageName: string]: LocalPackage}
163
184
  */
164
- static async getLocalPackageMap(dir: string, globIgnorePatterns: string[] = []): Promise<LocalPackageMap> {
165
- const packageMap: { [packageName: string]: LocalPackage } = {};
166
- const filePaths = await Fs.getFilePathsMatchingGlob(dir, '**/package.json', [
167
- '**/node_modules/**',
168
- '**/dist/**',
169
- ...globIgnorePatterns,
170
- ]);
171
- for (const filePath of filePaths) {
172
- const packageJson = JSON.parse(await Fs.readFile(filePath));
185
+ static async getLocalPackageMap(dir: string): Promise<LocalPackageMap> {
186
+ return PackageUtil.toNameKeyedMap(await PackageUtil.getLocalPackagePathMap(dir));
187
+ }
188
+
189
+ /**
190
+ * Every member package of the workspace rooted at `dir`, keyed by package.json path — the
191
+ * package's IDENTITY. Same-named packages (every workspace root is named `root`) are distinct
192
+ * entries here; anything that enumerates workspace members must start from this map, not from
193
+ * the name-keyed index.
194
+ *
195
+ * Membership is DECLARED, never crawled — `WorkspaceDeclaration` is the one owner: a root's
196
+ * lerna.json `packages` (plus the root package.json's `workspacePackages` extra roots) names
197
+ * exactly its members, a declared root owns its subtree, and only a tree with no declaration
198
+ * at its root is walked (bounded: never into hidden directories, `node_modules`, `dist`, or
199
+ * through symlinks — the unbounded-glob OOM of 2026-09-02 stays fixed). A same-named
200
+ * package.json nobody declared (the CI fixture trees that shadowed the app's packages in
201
+ * n3xah/app run 33747781291) is not a member; a leaf-name collision among members is a hard
202
+ * error naming both paths.
203
+ *
204
+ * @param dir dir path that contains local packages
205
+ * @returns {[packageJsonPath: string]: LocalPackage}
206
+ */
207
+ static async getLocalPackagePathMap(dir: string): Promise<LocalPackagePathMap> {
208
+ const packagePathMap: LocalPackagePathMap = {};
209
+ for (const { filePath, packageJson } of await WorkspaceDeclaration.discover(dir)) {
173
210
  const name = packageJson['name'];
174
- packageMap[name] = {
211
+ packagePathMap[filePath] = {
175
212
  name,
176
213
  filePath,
177
214
  packageJson,
@@ -186,7 +223,7 @@ export class PackageUtil {
186
223
  const workspaceLernaJson = (await Fs.exists(workspaceLernaJsonPath))
187
224
  ? JSON.parse(await Fs.readFile(workspaceLernaJsonPath))
188
225
  : undefined;
189
- packageMap[name].workspace = {
226
+ packagePathMap[filePath].workspace = {
190
227
  path: workspacePath,
191
228
  rootPackageJson: workspacePackageJson,
192
229
  lernaJson: workspaceLernaJson,
@@ -194,7 +231,7 @@ export class PackageUtil {
194
231
  }
195
232
  }
196
233
 
197
- return packageMap;
234
+ return packagePathMap;
198
235
  }
199
236
 
200
237
  /**
@@ -309,7 +346,8 @@ export class PackageUtil {
309
346
  * @returns `WorkspaceMetadata`
310
347
  */
311
348
  static async getWorkspaceMetadata(workspacePath: string): Promise<WorkspaceMetadata> {
312
- const packageMap = await PackageUtil.getLocalPackageMap(workspacePath);
349
+ const packagePathMap = await PackageUtil.getLocalPackagePathMap(workspacePath);
350
+ const packageMap = PackageUtil.toNameKeyedMap(packagePathMap);
313
351
  const packageGraph = await PackageUtil.getPackageDependencyGraph(packageMap);
314
352
  const sortedPackageNames = PackageUtil.getDependencyOrder(packageGraph).filter(
315
353
  (packageName) => !!packageMap[packageName]
@@ -317,6 +355,7 @@ export class PackageUtil {
317
355
  const workspaceToPackageMap = PackageUtil.getWorkspaceToPackageMap(packageMap);
318
356
  return {
319
357
  packageMap,
358
+ packagePathMap,
320
359
  packageGraph,
321
360
  sortedPackageNames,
322
361
  workspaceToPackageMap,
@@ -359,17 +398,18 @@ export class PackageUtil {
359
398
  /**
360
399
  * Compute the transitive closure of workspace dependencies for `localPackage`.
361
400
  *
362
- * Reuses `getPackageDependencyGraph`, which already crawls `dependencies` and
363
- * `devDependencies` (transitively, across the whole workspace) and adds an
364
- * edge `consumer -> dependency` for each. So the workspace deps reachable from
365
- * a package are exactly the nodes reachable by following `successors` from its
366
- * node. We do a cycle-safe traversal (a `visited` set), since the dependency
367
- * graph can contain cycles. The package itself is excluded from the result.
401
+ * Traverses dependency DECLARATIONS (`dependencies` + `devDependencies`) starting from THIS
402
+ * package instance's own package.json, resolving each declared name through
403
+ * `localPackageMap` and expanding through the resolved packages' declarations in turn.
404
+ * Cycle safety and the visited set are PATH-KEYED (package.json paths): identity is the
405
+ * path, never the name. The closure is never seeded from a shared name-keyed graph node —
406
+ * same-named packages (every workspace root is named `root`) share such a node, so a
407
+ * graph-seeded closure conflates their dependencies and computes another package's closure
408
+ * for whichever same-named instance lost the name-keyed collision.
368
409
  *
369
- * Only names present in `localPackageMap` are returned — i.e. packages that
370
- * actually live in the workspace and can be symlinked (the graph already
371
- * filters to file:/relative/workspace deps in `addDependencies`, but we filter
372
- * again here so the result is exactly the set of linkable packages).
410
+ * Only names present in `localPackageMap` are traversed and returned — i.e. packages that
411
+ * actually live in the workspace and can be symlinked. The package itself is excluded from
412
+ * the result (its own path seeds the visited set).
373
413
  *
374
414
  * Public: WorkspaceDoctor (@proteinjs/build) diagnoses the same closure this method links —
375
415
  * verification and repair must agree on the set of packages that ought to be symlinked.
@@ -380,25 +420,24 @@ export class PackageUtil {
380
420
  localPackage: LocalPackage,
381
421
  localPackageMap: LocalPackageMap
382
422
  ): Promise<string[]> {
383
- const graph = await PackageUtil.getPackageDependencyGraph(localPackageMap);
384
- const rootPackageName = localPackage.packageJson['name'];
385
-
386
423
  const transitiveDependencies = new Set<string>();
387
- const visited = new Set<string>([rootPackageName]);
388
- const stack: string[] = [rootPackageName];
424
+ const visitedPaths = new Set<string>([localPackage.filePath]);
425
+ const stack: LocalPackage[] = [localPackage];
389
426
  while (stack.length > 0) {
390
427
  const current = stack.pop()!;
391
- const successors = (graph.successors(current) as string[] | void) || [];
392
- for (const dependencyPackageName of successors) {
393
- if (visited.has(dependencyPackageName)) {
428
+ const declared = {
429
+ ...(current.packageJson['dependencies'] ?? {}),
430
+ ...(current.packageJson['devDependencies'] ?? {}),
431
+ };
432
+ for (const dependencyPackageName of Object.keys(declared)) {
433
+ const dependencyPackage = localPackageMap[dependencyPackageName];
434
+ if (!dependencyPackage || visitedPaths.has(dependencyPackage.filePath)) {
394
435
  continue;
395
436
  }
396
437
 
397
- visited.add(dependencyPackageName);
398
- stack.push(dependencyPackageName);
399
- if (localPackageMap[dependencyPackageName]) {
400
- transitiveDependencies.add(dependencyPackageName);
401
- }
438
+ visitedPaths.add(dependencyPackage.filePath);
439
+ transitiveDependencies.add(dependencyPackageName);
440
+ stack.push(dependencyPackage);
402
441
  }
403
442
  }
404
443
 
@@ -527,4 +566,19 @@ export class PackageUtil {
527
566
  }
528
567
  }
529
568
  }
569
+
570
+ /**
571
+ * Derive the name-keyed resolution index from the path-keyed discovery map. When several
572
+ * packages share a name, the lexicographically last package.json path wins — deterministic,
573
+ * and irrelevant for real dependency names (nothing declares a dep on `root`; uniquely-named
574
+ * packages resolve identically either way).
575
+ */
576
+ private static toNameKeyedMap(packagePathMap: LocalPackagePathMap): LocalPackageMap {
577
+ const packageMap: LocalPackageMap = {};
578
+ for (const filePath of Object.keys(packagePathMap).sort()) {
579
+ const localPackage = packagePathMap[filePath];
580
+ packageMap[localPackage.name] = localPackage;
581
+ }
582
+ return packageMap;
583
+ }
530
584
  }
@@ -0,0 +1,340 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs/promises';
3
+
4
+ /** A declared-workspace rule was broken. The message names every path involved. */
5
+ export class WorkspaceDeclarationError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = 'WorkspaceDeclarationError';
9
+ Object.setPrototypeOf(this, WorkspaceDeclarationError.prototype); // ES5 target: keep instanceof true across the Error subclass
10
+ }
11
+ }
12
+
13
+ export type DiscoveredPackage = {
14
+ /** Absolute package.json path — the package's identity. */
15
+ filePath: string;
16
+ packageJson: any;
17
+ /** `declared`: named by a workspace declaration; `crawled`: found by the bounded walk of an undeclared tree. */
18
+ origin: 'declared' | 'crawled';
19
+ /** The declaration file (lerna.json or the root package.json) that named a `declared` package. */
20
+ declaredBy?: string;
21
+ };
22
+
23
+ type Declaration = {
24
+ rootDir: string;
25
+ /** Each pattern with the file that declared it, in declaration order. */
26
+ patterns: Array<{ pattern: string; source: string }>;
27
+ };
28
+
29
+ /**
30
+ * The ONE owner of workspace membership: which package.json files belong to the workspace rooted
31
+ * at a directory.
32
+ *
33
+ * Membership is DECLARED, never crawled. A directory holding a `lerna.json` (its `packages`
34
+ * globs; lerna's own default `packages/*` when the field is absent) or a root `package.json`
35
+ * carrying `workspacePackages` (extra roots the workspace tooling builds alongside lerna's
36
+ * members — e.g. an app's `ops/*` tools that lerna must not version) is a workspace ROOT. Its
37
+ * members are exactly what it declares, and it OWNS its subtree: nothing undeclared beneath it is
38
+ * a member, however the tree is walked from above. A member that is itself a root expands through
39
+ * its own declaration (the metarepo's nested lerna roots).
40
+ *
41
+ * Why: the previous crawl (every package.json under the root, keyed by name, last path wins)
42
+ * let a CI fixture tree whose package.json files reused the app's own package names shadow the
43
+ * real packages — build-workspace built "1 package in workspace" and the release image shipped
44
+ * with no server dist (n3xah/app Deploy to Test run 33747781291, 2026-09-03). A declaration
45
+ * cannot be shadowed by a file nobody declared.
46
+ *
47
+ * A tree with no declaration at its root (the n3xa metarepo root) still crawls — bounded exactly
48
+ * as before: never into hidden directories, `node_modules`, `dist`, or through symlinks — and
49
+ * hands every declared root it meets to that root's declaration.
50
+ *
51
+ * Hard errors, each naming its paths: a declared literal path with no package.json; two leaf
52
+ * packages sharing a name (dependencies resolve to members BY NAME). Containers — a package.json
53
+ * with members beneath it; every root in the metarepo is named `root` — are identified by path
54
+ * and exempt from the name rule.
55
+ *
56
+ * npm's own `workspaces` field is deliberately NOT read: it changes how `npm i` installs at the
57
+ * root, which is a different install model from the per-package installs this tooling performs.
58
+ */
59
+ export class WorkspaceDeclaration {
60
+ /** Root package.json field naming extra member globs beside lerna's `packages`. */
61
+ static readonly EXTRA_PACKAGES_FIELD = 'workspacePackages';
62
+ private static readonly lernaDefaultPackages = ['packages/*'];
63
+ private static readonly prunedDirNames = new Set(['node_modules', 'dist']);
64
+
65
+ /** Every member package.json of the workspace rooted at `rootDir`, sorted by path. */
66
+ static async discover(rootDir: string): Promise<DiscoveredPackage[]> {
67
+ const found = new Map<string, DiscoveredPackage>();
68
+ const root = path.resolve(rootDir);
69
+ const declaration = await WorkspaceDeclaration.read(root);
70
+ if (declaration) {
71
+ await WorkspaceDeclaration.discoverDeclared(declaration, found);
72
+ } else {
73
+ await WorkspaceDeclaration.crawl(root, found);
74
+ }
75
+ const discovered = Array.from(found.values()).sort((a, b) =>
76
+ a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0
77
+ );
78
+ WorkspaceDeclaration.assertNoLeafNameCollisions(discovered);
79
+ return discovered;
80
+ }
81
+
82
+ /** The declaration at `dir` when `dir` is a workspace root; undefined otherwise. */
83
+ static async read(dir: string): Promise<Declaration | undefined> {
84
+ const patterns: Declaration['patterns'] = [];
85
+ const lernaPath = path.join(dir, 'lerna.json');
86
+ const lernaJson = await WorkspaceDeclaration.readJsonIfExists(lernaPath);
87
+ if (lernaJson) {
88
+ const declared = Array.isArray(lernaJson.packages)
89
+ ? lernaJson.packages
90
+ : WorkspaceDeclaration.lernaDefaultPackages;
91
+ for (const pattern of declared) {
92
+ patterns.push({ pattern: WorkspaceDeclaration.assertPattern(pattern, lernaPath), source: lernaPath });
93
+ }
94
+ }
95
+ const packageJsonPath = path.join(dir, 'package.json');
96
+ const packageJson = await WorkspaceDeclaration.readJsonIfExists(packageJsonPath);
97
+ const extra = packageJson?.[WorkspaceDeclaration.EXTRA_PACKAGES_FIELD];
98
+ if (extra !== undefined) {
99
+ if (!Array.isArray(extra)) {
100
+ throw new WorkspaceDeclarationError(
101
+ `${packageJsonPath}: "${WorkspaceDeclaration.EXTRA_PACKAGES_FIELD}" must be an array of package globs`
102
+ );
103
+ }
104
+ for (const pattern of extra) {
105
+ patterns.push({
106
+ pattern: WorkspaceDeclaration.assertPattern(pattern, packageJsonPath),
107
+ source: packageJsonPath,
108
+ });
109
+ }
110
+ }
111
+ return patterns.length > 0 ? { rootDir: dir, patterns } : undefined;
112
+ }
113
+
114
+ /** A declared root: its own package.json (the container), then exactly its declared members. */
115
+ private static async discoverDeclared(declaration: Declaration, found: Map<string, DiscoveredPackage>) {
116
+ const own = path.join(declaration.rootDir, 'package.json');
117
+ if (await WorkspaceDeclaration.exists(own)) {
118
+ await WorkspaceDeclaration.add(found, own, 'declared', declaration.patterns[0].source);
119
+ }
120
+ for (const { pattern, source } of declaration.patterns) {
121
+ for (const memberDir of await WorkspaceDeclaration.expand(declaration.rootDir, pattern, source)) {
122
+ const nested = await WorkspaceDeclaration.read(memberDir);
123
+ if (nested) {
124
+ await WorkspaceDeclaration.discoverDeclared(nested, found); // a nested root owns its subtree
125
+ } else {
126
+ await WorkspaceDeclaration.add(found, path.join(memberDir, 'package.json'), 'declared', source);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * The bounded walk of an UNDECLARED tree (the metarepo root): every package.json it meets is a
134
+ * member, and every declared root it meets is handed to that root's declaration instead of
135
+ * being walked. Never enters hidden directories, node_modules, or dist; never follows symlinks
136
+ * (`withFileTypes` reports a symlink as neither directory nor file).
137
+ */
138
+ private static async crawl(rootDir: string, found: Map<string, DiscoveredPackage>) {
139
+ const pending: string[] = [rootDir];
140
+ while (pending.length > 0) {
141
+ const currentDir = pending.pop()!;
142
+ for (const dirent of await fs.readdir(currentDir, { withFileTypes: true })) {
143
+ const entryPath = path.join(currentDir, dirent.name);
144
+ if (dirent.isDirectory()) {
145
+ if (WorkspaceDeclaration.isPrunedDirName(dirent.name)) {
146
+ continue;
147
+ }
148
+ const declaration = await WorkspaceDeclaration.read(entryPath);
149
+ if (declaration) {
150
+ await WorkspaceDeclaration.discoverDeclared(declaration, found);
151
+ } else {
152
+ pending.push(entryPath);
153
+ }
154
+ } else if (dirent.isFile() && dirent.name === 'package.json') {
155
+ await WorkspaceDeclaration.add(found, entryPath, 'crawled');
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * The directories a declared pattern names under `rootDir`, lerna's semantics: a literal path
163
+ * MUST hold a package.json (hard error otherwise — a declared package that does not exist is
164
+ * a broken declaration, never a silent skip); a glob (`*` within a segment, `**` for any depth)
165
+ * matches directories that hold one, walking with the same prune rules as the crawl. A declared
166
+ * root met inside a glob's walk is matched as a whole and not walked past — it owns its subtree.
167
+ */
168
+ private static async expand(rootDir: string, pattern: string, source: string): Promise<string[]> {
169
+ const segments = pattern.split('/').filter((segment) => segment.length > 0 && segment !== '.');
170
+ if (!segments.some((segment) => segment.includes('*'))) {
171
+ const dir = path.join(rootDir, ...segments);
172
+ if (!(await WorkspaceDeclaration.exists(path.join(dir, 'package.json')))) {
173
+ throw new WorkspaceDeclarationError(
174
+ `${source} declares "${pattern}" but ${path.join(dir, 'package.json')} does not exist — a declared package must exist (fix the declaration or restore the package)`
175
+ );
176
+ }
177
+ return [dir];
178
+ }
179
+ const matches = new Set<string>();
180
+ await WorkspaceDeclaration.matchSegments(rootDir, segments, 0, matches);
181
+ return Array.from(matches).sort();
182
+ }
183
+
184
+ private static async matchSegments(dir: string, segments: string[], index: number, matches: Set<string>) {
185
+ if (index === segments.length) {
186
+ if (await WorkspaceDeclaration.exists(path.join(dir, 'package.json'))) {
187
+ matches.add(dir);
188
+ }
189
+ return;
190
+ }
191
+ const segment = segments[index];
192
+ const remaining = segments.length - index - 1;
193
+ if (segment === '**') {
194
+ await WorkspaceDeclaration.matchSegments(dir, segments, index + 1, matches); // zero directories
195
+ for (const child of await WorkspaceDeclaration.childDirs(dir)) {
196
+ if (await WorkspaceDeclaration.read(child)) {
197
+ matches.add(child); // a nested root owns its subtree — matched whole, never walked past
198
+ continue;
199
+ }
200
+ await WorkspaceDeclaration.matchSegments(child, segments, index, matches); // one more directory under **
201
+ }
202
+ return;
203
+ }
204
+ const children = segment.includes('*')
205
+ ? (await WorkspaceDeclaration.childDirs(dir)).filter((child) =>
206
+ WorkspaceDeclaration.segmentMatcher(segment).test(path.basename(child))
207
+ )
208
+ : (await WorkspaceDeclaration.isRealDir(path.join(dir, segment)))
209
+ ? [path.join(dir, segment)]
210
+ : [];
211
+ for (const child of children) {
212
+ if (remaining > 0 && (await WorkspaceDeclaration.read(child))) {
213
+ matches.add(child);
214
+ continue;
215
+ }
216
+ await WorkspaceDeclaration.matchSegments(child, segments, index + 1, matches);
217
+ }
218
+ }
219
+
220
+ /** Leaf packages resolve by name; two leaves with one name is a broken workspace, not a tie to break. */
221
+ private static assertNoLeafNameCollisions(discovered: DiscoveredPackage[]) {
222
+ const dirs = discovered.map((entry) => path.dirname(entry.filePath));
223
+ const isContainer = (entry: DiscoveredPackage) => {
224
+ const prefix = path.dirname(entry.filePath) + path.sep;
225
+ return dirs.some((dir) => dir.startsWith(prefix));
226
+ };
227
+ const byName = new Map<string, DiscoveredPackage[]>();
228
+ for (const entry of discovered) {
229
+ const name = entry.packageJson?.name;
230
+ if (typeof name !== 'string' || name.length === 0) {
231
+ continue;
232
+ }
233
+ byName.set(name, (byName.get(name) ?? []).concat(entry));
234
+ }
235
+ const collisions: string[] = [];
236
+ for (const [name, entries] of Array.from(byName.entries())) {
237
+ const leaves = entries.filter((entry) => !isContainer(entry));
238
+ if (leaves.length < 2) {
239
+ continue;
240
+ }
241
+ collisions.push(
242
+ `"${name}" is claimed by\n` +
243
+ leaves
244
+ .map(
245
+ (entry) =>
246
+ ` - ${entry.filePath} (${entry.origin === 'declared' ? `declared by ${entry.declaredBy}` : 'crawled — no declaration names it'})`
247
+ )
248
+ .join('\n')
249
+ );
250
+ }
251
+ if (collisions.length > 0) {
252
+ throw new WorkspaceDeclarationError(
253
+ `Workspace package name collision — dependencies resolve to workspace members BY NAME, so two packages cannot share one:\n${collisions.join('\n')}\nRename the impostor (fixtures carry fixture names) or move it outside the workspace.`
254
+ );
255
+ }
256
+ }
257
+
258
+ private static async add(
259
+ found: Map<string, DiscoveredPackage>,
260
+ filePath: string,
261
+ origin: DiscoveredPackage['origin'],
262
+ declaredBy?: string
263
+ ) {
264
+ if (found.has(filePath)) {
265
+ return;
266
+ }
267
+ const packageJson = JSON.parse(await fs.readFile(filePath, 'utf8'));
268
+ found.set(filePath, { filePath, packageJson, origin, ...(declaredBy ? { declaredBy } : {}) });
269
+ }
270
+
271
+ private static assertPattern(pattern: unknown, source: string): string {
272
+ if (typeof pattern !== 'string' || pattern.trim().length === 0) {
273
+ throw new WorkspaceDeclarationError(
274
+ `${source}: package patterns must be non-empty strings, got ${JSON.stringify(pattern)}`
275
+ );
276
+ }
277
+ if (path.isAbsolute(pattern) || pattern.split('/').includes('..')) {
278
+ throw new WorkspaceDeclarationError(
279
+ `${source}: package pattern "${pattern}" must be relative to the root and stay inside it`
280
+ );
281
+ }
282
+ return pattern;
283
+ }
284
+
285
+ private static segmentMatcher(segment: string): RegExp {
286
+ const escaped = segment.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
287
+ return new RegExp(`^${escaped}$`);
288
+ }
289
+
290
+ private static isPrunedDirName(name: string): boolean {
291
+ return name.startsWith('.') || WorkspaceDeclaration.prunedDirNames.has(name);
292
+ }
293
+
294
+ /** Real (non-symlink, non-pruned) child directories of `dir`. */
295
+ private static async childDirs(dir: string): Promise<string[]> {
296
+ let dirents;
297
+ try {
298
+ dirents = await fs.readdir(dir, { withFileTypes: true });
299
+ } catch {
300
+ return [];
301
+ }
302
+ return dirents
303
+ .filter((dirent) => dirent.isDirectory() && !WorkspaceDeclaration.isPrunedDirName(dirent.name))
304
+ .map((dirent) => path.join(dir, dirent.name))
305
+ .sort();
306
+ }
307
+
308
+ private static async isRealDir(p: string): Promise<boolean> {
309
+ try {
310
+ return (await fs.lstat(p)).isDirectory();
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ private static async exists(p: string): Promise<boolean> {
317
+ try {
318
+ await fs.access(p);
319
+ return true;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+
325
+ private static async readJsonIfExists(p: string): Promise<any | undefined> {
326
+ let text: string;
327
+ try {
328
+ text = await fs.readFile(p, 'utf8');
329
+ } catch {
330
+ return undefined;
331
+ }
332
+ try {
333
+ return JSON.parse(text);
334
+ } catch (error) {
335
+ throw new WorkspaceDeclarationError(
336
+ `${p} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
337
+ );
338
+ }
339
+ }
340
+ }
@@ -0,0 +1,35 @@
1
+ import { parseArgsMap } from '../src/ArgsMap';
2
+
3
+ describe('parseArgsMap', () => {
4
+ test('preserves the full value when it contains embedded = characters', () => {
5
+ // The estate CLI's --note values regularly carry '=' (e.g. env assignments, key=value notes).
6
+ // The parser must split on the FIRST '=' only and keep the remainder intact.
7
+ const args = parseArgsMap(['--note=emulator=spanner-x port=9040']);
8
+ expect(args['note']).toEqual('emulator=spanner-x port=9040');
9
+ });
10
+
11
+ test('parses a simple key=value pair', () => {
12
+ const args = parseArgsMap(['--name=my-estate']);
13
+ expect(args['name']).toEqual('my-estate');
14
+ });
15
+
16
+ test('parses a bare flag as boolean true', () => {
17
+ const args = parseArgsMap(['--pin']);
18
+ expect(args['pin']).toEqual(true);
19
+ });
20
+
21
+ test('parses an explicitly empty value as the empty string', () => {
22
+ const args = parseArgsMap(['--note=']);
23
+ expect(args['note']).toEqual('');
24
+ });
25
+
26
+ test('ignores args that do not start with --', () => {
27
+ const args = parseArgsMap(['positional', '-x', '--keep=yes']);
28
+ expect(args).toEqual({ keep: 'yes' });
29
+ });
30
+
31
+ test('parses multiple args together, preserving each full value', () => {
32
+ const args = parseArgsMap(['--containers=spanner-x', '--pids=123', '--pin', '--note=a=b=c']);
33
+ expect(args).toEqual({ containers: 'spanner-x', pids: '123', pin: true, note: 'a=b=c' });
34
+ });
35
+ });