@testspectra/cli 1.0.70 → 1.1.0-rc.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 (427) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +36 -270
  3. package/bin/.testspectra-runner.hash +1 -0
  4. package/bin/spectra.js +1 -1
  5. package/bin/testspectra-runner +0 -0
  6. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  7. package/demo-app/assets/index-DOtRypCa.js +154 -0
  8. package/demo-app/index.html +2 -2
  9. package/dist/.tsbuildinfo +1 -0
  10. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  11. package/dist/commands/__tests__/doctor.test.js +39 -0
  12. package/dist/commands/__tests__/init.test.d.ts +1 -0
  13. package/dist/commands/__tests__/init.test.js +148 -0
  14. package/dist/commands/__tests__/license.test.d.ts +1 -0
  15. package/dist/commands/__tests__/license.test.js +13 -0
  16. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  17. package/dist/commands/__tests__/refactor.test.js +64 -0
  18. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  19. package/dist/commands/__tests__/run-e2e.test.js +422 -0
  20. package/dist/commands/__tests__/run.test.d.ts +1 -0
  21. package/dist/commands/__tests__/run.test.js +273 -0
  22. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  23. package/dist/commands/__tests__/watch.test.js +155 -0
  24. package/dist/commands/add.js +86 -85
  25. package/dist/commands/demo.js +21 -11
  26. package/dist/commands/devices.d.ts +14 -2
  27. package/dist/commands/devices.js +44 -34
  28. package/dist/commands/doctor.d.ts +3 -1
  29. package/dist/commands/doctor.js +43 -264
  30. package/dist/commands/init.d.ts +1 -1
  31. package/dist/commands/init.js +211 -196
  32. package/dist/commands/install.d.ts +4 -0
  33. package/dist/commands/install.js +59 -0
  34. package/dist/commands/license.d.ts +4 -0
  35. package/dist/commands/license.js +30 -0
  36. package/dist/commands/lint.d.ts +5 -0
  37. package/dist/commands/lint.js +55 -0
  38. package/dist/commands/refactor.d.ts +9 -0
  39. package/dist/commands/refactor.js +172 -0
  40. package/dist/commands/run.d.ts +23 -2
  41. package/dist/commands/run.js +322 -111
  42. package/dist/commands/skills.d.ts +9 -0
  43. package/dist/commands/skills.js +28 -0
  44. package/dist/commands/sync-types.d.ts +4 -0
  45. package/dist/commands/sync-types.js +15 -0
  46. package/dist/commands/test.d.ts +17 -0
  47. package/dist/commands/test.js +76 -0
  48. package/dist/commands/update.js +39 -43
  49. package/dist/commands/watch.d.ts +6 -0
  50. package/dist/commands/watch.js +55 -45
  51. package/dist/config/loader.d.ts +1 -1
  52. package/dist/config/loader.js +40 -25
  53. package/dist/config/schema.d.ts +104 -145
  54. package/dist/config/schema.js +36 -45
  55. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  56. package/dist/generator/__tests__/tsconfig-isolation.test.js +282 -0
  57. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  58. package/dist/generator/__tests__/type-generator.test.js +386 -0
  59. package/dist/generator/index.d.ts +2 -0
  60. package/dist/generator/index.js +2 -0
  61. package/dist/generator/tsconfig-generator.d.ts +27 -0
  62. package/dist/generator/tsconfig-generator.js +419 -0
  63. package/dist/generator/type-generator.d.ts +59 -0
  64. package/dist/generator/type-generator.js +740 -0
  65. package/dist/index.d.ts +9 -5
  66. package/dist/index.js +165 -51
  67. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  68. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  69. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  70. package/dist/lifecycle/hook-discovery.js +60 -0
  71. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  72. package/dist/lifecycle/hook-dispatcher.js +190 -0
  73. package/dist/lifecycle/index.d.ts +3 -0
  74. package/dist/lifecycle/index.js +3 -0
  75. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  76. package/dist/lifecycle/parallel-grouping.js +151 -0
  77. package/dist/linter/__tests__/anonymous-export.test.d.ts +1 -0
  78. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  79. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  80. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  81. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  82. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  83. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  84. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  85. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  86. package/dist/linter/__tests__/step-parity.test.js +372 -0
  87. package/dist/linter/discovery.d.ts +30 -0
  88. package/dist/linter/discovery.js +249 -0
  89. package/dist/linter/extractor.d.ts +67 -0
  90. package/dist/linter/extractor.js +245 -0
  91. package/dist/linter/index.d.ts +7 -0
  92. package/dist/linter/index.js +7 -0
  93. package/dist/linter/parser.d.ts +24 -0
  94. package/dist/linter/parser.js +219 -0
  95. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  96. package/dist/linter/schemas/spec.schema.js +150 -0
  97. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  98. package/dist/linter/schemas/suite.schema.js +83 -0
  99. package/dist/linter/spec-linter.d.ts +23 -0
  100. package/dist/linter/spec-linter.js +630 -0
  101. package/dist/linter/types.d.ts +28 -0
  102. package/dist/linter/types.js +1 -0
  103. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  104. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  105. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  106. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  107. package/dist/mobile/device-pool.d.ts +23 -0
  108. package/dist/mobile/device-pool.js +1 -0
  109. package/dist/mobile/index.d.ts +1 -0
  110. package/dist/mobile/index.js +1 -0
  111. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  112. package/dist/refactor/__tests__/refactor-engine.test.js +873 -0
  113. package/dist/refactor/index.d.ts +1 -0
  114. package/dist/refactor/index.js +1 -0
  115. package/dist/refactor/refactor-engine.d.ts +50 -0
  116. package/dist/refactor/refactor-engine.js +587 -0
  117. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  118. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  119. package/dist/reporter/semantic-formatter.d.ts +6 -0
  120. package/dist/reporter/semantic-formatter.js +206 -0
  121. package/dist/reporter/summary-view.d.ts +12 -0
  122. package/dist/reporter/summary-view.js +19 -0
  123. package/dist/reporter/terminal-stream.d.ts +32 -0
  124. package/dist/reporter/terminal-stream.js +82 -0
  125. package/dist/reporter/types.d.ts +2 -0
  126. package/dist/reporter/types.js +1 -0
  127. package/dist/runner/bridge.d.ts +10 -5
  128. package/dist/runner/bridge.js +101 -45
  129. package/dist/runner/reporter.d.ts +32 -2
  130. package/dist/runner/reporter.js +427 -13
  131. package/dist/server/demo.js +43 -44
  132. package/dist/types/generator.d.ts +1 -17
  133. package/dist/types/generator.js +1 -443
  134. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  135. package/dist/utils/__tests__/api-server.test.js +62 -0
  136. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  137. package/dist/utils/__tests__/demo-server.test.js +35 -0
  138. package/dist/utils/api-server.d.ts +19 -0
  139. package/dist/utils/api-server.js +179 -0
  140. package/dist/utils/demo-server.d.ts +5 -0
  141. package/dist/utils/demo-server.js +46 -36
  142. package/package.json +25 -13
  143. package/templates/default/CLAUDE.md +12 -5
  144. package/templates/default/fixtures/apiData.json +24 -0
  145. package/templates/default/package.json +12 -3
  146. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  147. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  148. package/templates/default/page-objects/MatchersPage/mobile.ts +66 -17
  149. package/templates/default/page-objects/MatchersPage/web.ts +22 -2
  150. package/templates/default/page-objects/NavigationPage/mobile.ts +11 -2
  151. package/templates/default/page-objects/NavigationPage/web.ts +1 -1
  152. package/templates/default/page-objects/PlaygroundPage/mobile.ts +34 -18
  153. package/templates/default/page-objects/PlaygroundPage/web.ts +4 -2
  154. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +53 -0
  155. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +19 -0
  156. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +31 -0
  157. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  158. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  159. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +27 -0
  160. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +19 -0
  161. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +26 -0
  162. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  163. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  164. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  165. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  166. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  167. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  168. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  169. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  170. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  171. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  172. package/templates/default/specs/ApiInterception/suite.md +10 -0
  173. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  174. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  175. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  176. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  177. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  178. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  179. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  180. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  181. package/templates/default/specs/Authentication/suite.md +9 -0
  182. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  183. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
  184. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  185. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  186. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  187. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  188. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  189. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
  190. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  191. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
  192. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +9 -4
  193. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  194. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  195. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  196. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  197. package/templates/default/specs/DeepLink/suite.md +9 -0
  198. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  199. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
  200. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  201. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  202. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
  203. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  204. package/templates/default/specs/ElementMatchers/suite.md +2 -1
  205. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  206. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  207. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  208. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  209. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  210. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  211. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  212. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  213. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  214. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
  215. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +4 -4
  216. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  217. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
  218. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +3 -3
  219. package/templates/default/spectra.config.ts +22 -31
  220. package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
  221. package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
  222. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  223. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  224. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +3 -1
  225. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +3 -3
  226. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +4 -2
  227. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +4 -4
  228. package/templates/default/tsconfig.json +3 -1
  229. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  230. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  231. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  232. package/templates/nx/.nx/workspace-data/d/daemon.log +5366 -0
  233. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  234. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  235. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  236. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  237. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  238. package/templates/nx/.nx/workspace-data/project-graph.json +14389 -0
  239. package/templates/nx/CLAUDE.md +2 -2
  240. package/templates/nx/package.json +6 -4
  241. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +27 -0
  242. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  243. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  244. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +24 -0
  245. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  246. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  247. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  248. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  249. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  250. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  251. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +15 -5
  252. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  253. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +9 -4
  254. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  255. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  256. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  257. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  258. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  259. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  260. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  261. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  262. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +4 -4
  263. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  264. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +3 -3
  265. package/templates/nx/pnpm-lock.yaml +6663 -0
  266. package/templates/nx/pnpm-workspace.yaml +2 -1
  267. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  268. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +13 -1
  269. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +85 -17
  270. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  271. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  272. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  273. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +4 -2
  274. package/templates/nx/shared/testing/project.json +19 -0
  275. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  276. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  277. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  278. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +3 -3
  279. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +2 -2
  280. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +4 -4
  281. package/templates/nx/spectra.config.ts +20 -29
  282. package/templates/nx/tsconfig.json +3 -1
  283. package/templates/react-component/.cursorrules +17 -0
  284. package/templates/react-component/.vscode/extensions.json +5 -0
  285. package/templates/react-component/CLAUDE.md +14 -0
  286. package/templates/react-component/README.md +69 -0
  287. package/templates/react-component/fixtures/component-mock.json +12 -0
  288. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  289. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  290. package/templates/react-component/package.json +27 -0
  291. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  292. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  293. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  294. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  295. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  296. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  297. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  298. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  299. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  300. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  301. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  302. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  303. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  304. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  305. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  306. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  307. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  308. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  309. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  310. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  311. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  312. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  313. package/templates/react-component/spectra.config.ts +27 -0
  314. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  315. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  316. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  317. package/templates/react-component/src/test-utils.tsx +23 -0
  318. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  319. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  320. package/templates/react-component/tsconfig.json +8 -0
  321. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  322. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  323. package/dist/index.cjs +0 -101
  324. package/dist/plugin.cjs +0 -575
  325. package/dist/plugin.d.ts +0 -16
  326. package/dist/plugin.js +0 -184
  327. package/dist/step/index.d.ts +0 -6
  328. package/dist/step/index.js +0 -6
  329. package/dist/step/matchers.d.ts +0 -14
  330. package/dist/step/matchers.js +0 -114
  331. package/dist/step/proto.d.ts +0 -46
  332. package/dist/step/proto.js +0 -114
  333. package/dist/step/runner/collection.d.ts +0 -13
  334. package/dist/step/runner/collection.js +0 -51
  335. package/dist/step/runner/single.d.ts +0 -21
  336. package/dist/step/runner/single.js +0 -138
  337. package/dist/step/spectra.d.ts +0 -34
  338. package/dist/step/spectra.js +0 -156
  339. package/dist/step/types.d.ts +0 -32
  340. package/dist/step/types.js +0 -5
  341. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  342. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  343. package/templates/default/tsconfig.spectra.android.json +0 -46
  344. package/templates/default/tsconfig.spectra.ios.json +0 -46
  345. package/templates/default/tsconfig.spectra.json +0 -22
  346. package/templates/default/tsconfig.spectra.web.json +0 -53
  347. package/templates/nx/fixtures/playgroundData.json +0 -7
  348. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  349. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  350. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  351. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  352. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  353. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  354. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  355. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  356. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  357. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  358. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  359. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  360. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  361. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  362. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  363. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  364. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  365. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  366. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  367. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  368. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  369. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  370. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  371. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  372. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  373. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  374. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  375. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  376. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  377. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  378. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  379. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  380. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  381. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  382. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  383. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  384. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  385. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  386. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  387. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  388. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  389. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  390. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  391. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  392. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  393. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  394. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  395. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  396. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  397. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  398. package/templates/nx/specs/Playground/suite.md +0 -9
  399. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  400. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  401. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  402. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  403. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  404. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  405. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  406. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  407. package/templates/nx/tsconfig.spectra.android.json +0 -39
  408. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  409. package/templates/nx/tsconfig.spectra.json +0 -25
  410. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  411. package/templates/nx/tsconfig.spectra.web.json +0 -45
  412. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  413. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  414. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  415. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  416. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  417. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  418. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  419. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  420. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  421. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  422. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  423. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  424. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  425. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  426. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  427. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -0,0 +1,740 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ConfigLoader } from '../config/loader.js';
4
+ import { TsConfigGenerator } from './tsconfig-generator.js';
5
+ export const PLATFORM_HIERARCHY = {
6
+ web: ['web', 'common'],
7
+ android: ['android', 'mobile', 'common'],
8
+ ios: ['ios', 'mobile', 'common'],
9
+ mobile: ['mobile', 'common'],
10
+ common: ['common'],
11
+ };
12
+ /**
13
+ * Discovers all feature directories and shared library directories.
14
+ * Standard discovery hierarchy:
15
+ * 1. Nx / Monorepo Project Descriptors (`project.json`):
16
+ * - projectType: "library" OR tags: ["type:shared", "type:library", "scope:shared"] => Shared Library Scope (isShared: true)
17
+ * - projectType: "application" OR tags: ["type:e2e", "type:app"] => Feature Scope (isShared: false)
18
+ * 2. Explicit User Configuration (`tsconfig.spectra.shared.json`):
19
+ * - Custom include path for shared testing utilities
20
+ * 3. Dynamic Workspace Filesystem Discovery (Fallback for non-Nx / custom layouts):
21
+ * - Directory with specs/e2e/tests subfolders => Feature Scope
22
+ * - Directory with page-objects/support/fixtures and NO specs => Shared Scope
23
+ */
24
+ export function discoverEntityScopes(cwd) {
25
+ const scopes = [];
26
+ const ignoredDirNames = new Set([
27
+ 'node_modules',
28
+ 'dist',
29
+ '.git',
30
+ '.testspectra',
31
+ 'templates',
32
+ 'src',
33
+ 'build',
34
+ 'coverage',
35
+ 'scripts',
36
+ '.nx',
37
+ '.turbo',
38
+ '.vscode',
39
+ ]);
40
+ // Priority 1: Scan for Nx `project.json` files throughout the workspace
41
+ function scanForNxProjects(dir, depth) {
42
+ if (depth <= 0 || !fs.existsSync(dir))
43
+ return;
44
+ const base = path.basename(dir);
45
+ if (ignoredDirNames.has(base))
46
+ return;
47
+ const projectJsonPath = path.join(dir, 'project.json');
48
+ if (fs.existsSync(projectJsonPath)) {
49
+ try {
50
+ const pObj = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
51
+ const projectType = pObj.projectType;
52
+ const tags = Array.isArray(pObj.tags) ? pObj.tags : [];
53
+ const isSharedLib = projectType === 'library' ||
54
+ tags.includes('type:shared') ||
55
+ tags.includes('type:library') ||
56
+ tags.includes('scope:shared');
57
+ const isE2eApp = projectType === 'application' ||
58
+ tags.includes('type:e2e') ||
59
+ tags.includes('type:app') ||
60
+ tags.some((t) => t.startsWith('scope:') && t !== 'scope:shared');
61
+ if (isSharedLib) {
62
+ if (!scopes.some((s) => s.dir === dir)) {
63
+ scopes.push({ name: 'shared', dir, isShared: true });
64
+ }
65
+ return; // Do not recurse into subdirectories of an identified project
66
+ }
67
+ else if (isE2eApp) {
68
+ // Extract scope name from tags (e.g. "scope:matchers" => "matchers") or project name
69
+ const scopeTag = tags.find((t) => t.startsWith('scope:') && t !== 'scope:shared');
70
+ let featureName = scopeTag ? scopeTag.replace('scope:', '') : '';
71
+ if (!featureName && pObj.name) {
72
+ featureName = pObj.name.replace(/-e2e$/, '').replace(/^e2e-/, '');
73
+ }
74
+ if (!featureName) {
75
+ featureName = path.basename(base === 'e2e' ? path.dirname(dir) : dir);
76
+ }
77
+ if (!scopes.some((s) => s.dir === dir || s.name === featureName)) {
78
+ scopes.push({ name: featureName, dir, isShared: false });
79
+ }
80
+ return;
81
+ }
82
+ }
83
+ catch { }
84
+ }
85
+ try {
86
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
87
+ for (const entry of entries) {
88
+ if (entry.isDirectory()) {
89
+ scanForNxProjects(path.join(dir, entry.name), depth - 1);
90
+ }
91
+ }
92
+ }
93
+ catch { }
94
+ }
95
+ scanForNxProjects(cwd, 5);
96
+ // If projects were discovered via project.json, we have explicit deterministic descriptors
97
+ if (scopes.length > 0) {
98
+ return scopes;
99
+ }
100
+ // Priority 2: Check if user-provided tsconfig.spectra.shared.json defines custom shared library paths
101
+ const userSharedConfig = path.join(cwd, 'tsconfig.spectra.shared.json');
102
+ if (fs.existsSync(userSharedConfig)) {
103
+ try {
104
+ const conf = JSON.parse(fs.readFileSync(userSharedConfig, 'utf-8'));
105
+ if (Array.isArray(conf.include)) {
106
+ for (const inc of conf.include) {
107
+ if (typeof inc === 'string' && inc.includes('**')) {
108
+ const baseCandidate = inc.split('/**')[0];
109
+ const resolved = path.resolve(cwd, baseCandidate);
110
+ if (fs.existsSync(resolved) && !scopes.some((s) => s.dir === resolved)) {
111
+ scopes.push({ name: 'shared', dir: resolved, isShared: true });
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ catch { }
118
+ }
119
+ // Priority 3: Fallback Directory Scan (for non-Nx or custom workspaces)
120
+ const entityDirNames = new Set(['page-objects', 'pageobjects', 'support', 'fixtures', 'global-hooks']);
121
+ function hasTestingEntities(dir) {
122
+ if (!fs.existsSync(dir))
123
+ return false;
124
+ const entityNames = ['page-objects', 'pageobjects', 'support', 'steps', 'actions', 'fixtures'];
125
+ return entityNames.some((name) => fs.existsSync(path.join(dir, name)));
126
+ }
127
+ function hasTestingSpecs(dir) {
128
+ if (!fs.existsSync(dir))
129
+ return false;
130
+ const specNames = ['specs', 'e2e', 'tests'];
131
+ return specNames.some((name) => fs.existsSync(path.join(dir, name)));
132
+ }
133
+ function fallbackScan(dir, depth) {
134
+ if (depth <= 0 || !fs.existsSync(dir))
135
+ return;
136
+ const base = path.basename(dir);
137
+ if (ignoredDirNames.has(base) || entityDirNames.has(base))
138
+ return;
139
+ if (dir !== cwd) {
140
+ if (base === 'e2e' || base === 'specs' || base === 'tests') {
141
+ const parentDir = path.dirname(dir);
142
+ if (parentDir !== cwd) {
143
+ const featureName = path.basename(parentDir);
144
+ if (!scopes.some((s) => s.dir === dir)) {
145
+ scopes.push({ name: featureName, dir, isShared: false });
146
+ }
147
+ return;
148
+ }
149
+ }
150
+ if (hasTestingSpecs(dir)) {
151
+ let targetDir = dir;
152
+ if (fs.existsSync(path.join(dir, 'e2e')))
153
+ targetDir = path.join(dir, 'e2e');
154
+ else if (fs.existsSync(path.join(dir, 'specs')))
155
+ targetDir = path.join(dir, 'specs');
156
+ else if (fs.existsSync(path.join(dir, 'tests')))
157
+ targetDir = path.join(dir, 'tests');
158
+ const featureName = path.basename(dir);
159
+ if (!scopes.some((s) => s.dir === targetDir || s.name === featureName)) {
160
+ scopes.push({ name: featureName, dir: targetDir, isShared: false });
161
+ }
162
+ return;
163
+ }
164
+ if (hasTestingEntities(dir) && !hasTestingSpecs(dir)) {
165
+ if (!scopes.some((s) => s.isShared && s.dir === dir)) {
166
+ scopes.push({ name: 'shared', dir, isShared: true });
167
+ return;
168
+ }
169
+ }
170
+ }
171
+ try {
172
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
173
+ for (const entry of entries) {
174
+ if (entry.isDirectory()) {
175
+ fallbackScan(path.join(dir, entry.name), depth - 1);
176
+ }
177
+ }
178
+ }
179
+ catch { }
180
+ }
181
+ fallbackScan(cwd, 5);
182
+ return scopes;
183
+ }
184
+ /**
185
+ * Detects a component-testing project (`@testspectra/react`'s Vitest browser-mode runner), via
186
+ * the same signal `cli/src/commands/test.ts` uses to gate `spectra test` itself. Used to augment
187
+ * the generated `Spectra` ambient type with `.mount()`, which only exists in that runtime.
188
+ */
189
+ export function isComponentTestingProject(cwd) {
190
+ try {
191
+ const pkgPath = path.join(cwd, 'package.json');
192
+ if (!fs.existsSync(pkgPath))
193
+ return false;
194
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
195
+ return Boolean(pkg.dependencies?.['@testspectra/react'] || pkg.devDependencies?.['@testspectra/react']);
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ export class TypeGenerator {
202
+ /**
203
+ * Main entry point to generate all ambient type declarations.
204
+ */
205
+ static generateAll(cwd) {
206
+ this.writeDeclarationFiles(cwd);
207
+ TsConfigGenerator.scaffoldConfigs(cwd);
208
+ }
209
+ /**
210
+ * Reloads `spectra.config.ts` and refreshes `.testspectra/types/env.d.ts` with the current
211
+ * `executionConfig.environmentVariables`. Separate from `writeDeclarationFiles`/`generateAll`
212
+ * (which must stay synchronous) because reading the config file is unavoidably async.
213
+ */
214
+ static async syncEnvDeclaration(cwd) {
215
+ const rootTypesDir = path.join(cwd, '.testspectra', 'types');
216
+ if (!fs.existsSync(rootTypesDir)) {
217
+ fs.mkdirSync(rootTypesDir, { recursive: true });
218
+ }
219
+ let environmentVariables = {};
220
+ try {
221
+ const config = await ConfigLoader.loadConfig(cwd);
222
+ environmentVariables = config.executionConfig?.environmentVariables ?? {};
223
+ }
224
+ catch { }
225
+ fs.writeFileSync(path.join(rootTypesDir, 'env.d.ts'), this.generateEnvDeclaration(environmentVariables), 'utf-8');
226
+ }
227
+ /**
228
+ * Discovers all feature directories and shared library directories.
229
+ * Returns a map of scope -> base directory
230
+ */
231
+ static getEntityScopes(cwd) {
232
+ return discoverEntityScopes(cwd);
233
+ }
234
+ static getPageObjectsDirsForScope(scopeDir, sharedDir) {
235
+ const dirs = [];
236
+ function collectPo(dir) {
237
+ if (!fs.existsSync(dir))
238
+ return;
239
+ const direct = [path.join(dir, 'page-objects'), path.join(dir, 'pageobjects')];
240
+ for (const d of direct) {
241
+ if (fs.existsSync(d) && !dirs.includes(d))
242
+ dirs.push(d);
243
+ }
244
+ try {
245
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
246
+ for (const entry of entries) {
247
+ if (entry.isDirectory() &&
248
+ entry.name !== 'node_modules' &&
249
+ entry.name !== 'dist' &&
250
+ entry.name !== '.git' &&
251
+ entry.name !== '.testspectra') {
252
+ const sub = [path.join(dir, entry.name, 'page-objects'), path.join(dir, entry.name, 'pageobjects')];
253
+ for (const s of sub) {
254
+ if (fs.existsSync(s) && !dirs.includes(s))
255
+ dirs.push(s);
256
+ }
257
+ }
258
+ }
259
+ }
260
+ catch { }
261
+ }
262
+ collectPo(scopeDir);
263
+ if (sharedDir && sharedDir !== scopeDir) {
264
+ collectPo(sharedDir);
265
+ }
266
+ return dirs;
267
+ }
268
+ static getEntityDirsForScope(entityType, scopeDir, sharedDir) {
269
+ const dirs = [];
270
+ function collectEntities(dir) {
271
+ if (!fs.existsSync(dir))
272
+ return;
273
+ const direct = [
274
+ path.join(dir, entityType),
275
+ path.join(dir, 'support', entityType),
276
+ path.join(dir, 'support', 'testing', entityType),
277
+ ];
278
+ for (const d of direct) {
279
+ if (fs.existsSync(d) && !dirs.includes(d))
280
+ dirs.push(d);
281
+ }
282
+ try {
283
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
284
+ for (const entry of entries) {
285
+ if (entry.isDirectory() &&
286
+ entry.name !== 'node_modules' &&
287
+ entry.name !== 'dist' &&
288
+ entry.name !== '.git' &&
289
+ entry.name !== '.testspectra') {
290
+ const sub = [path.join(dir, entry.name, entityType), path.join(dir, entry.name, 'support', entityType)];
291
+ for (const s of sub) {
292
+ if (fs.existsSync(s) && !dirs.includes(s))
293
+ dirs.push(s);
294
+ }
295
+ }
296
+ }
297
+ }
298
+ catch { }
299
+ }
300
+ collectEntities(scopeDir);
301
+ if (sharedDir && sharedDir !== scopeDir) {
302
+ collectEntities(sharedDir);
303
+ }
304
+ return dirs;
305
+ }
306
+ static generateDeclarationsForScope(cwd, outputTypesDir, scopeDir, sharedDir, rootTypesDir) {
307
+ const poDirs = this.getPageObjectsDirsForScope(scopeDir, sharedDir);
308
+ const actionsDirs = this.getEntityDirsForScope('actions', scopeDir, sharedDir);
309
+ const stepsDirs = this.getEntityDirsForScope('steps', scopeDir, sharedDir);
310
+ const isComponentTesting = isComponentTestingProject(cwd);
311
+ const platforms = ['web', 'android', 'ios', 'mobile', 'common'];
312
+ const results = {
313
+ web: '',
314
+ android: '',
315
+ ios: '',
316
+ mobile: '',
317
+ common: '',
318
+ };
319
+ // `env.d.ts` always lives at the workspace root types dir (env vars come from a single
320
+ // `spectra.config.ts`, unlike per-scope fixtures/page-objects), so a shared/feature scope's
321
+ // own declarations reference it via a relative path back up to the root.
322
+ let envRefPath = './env.d.ts';
323
+ if (rootTypesDir && rootTypesDir !== outputTypesDir) {
324
+ envRefPath = path.relative(outputTypesDir, path.join(rootTypesDir, 'env.d.ts')).replace(/\\/g, '/');
325
+ if (!envRefPath.startsWith('.'))
326
+ envRefPath = `./${envRefPath}`;
327
+ }
328
+ for (const platform of platforms) {
329
+ const hierarchy = PLATFORM_HIERARCHY[platform];
330
+ let content = `// Auto-generated ambient declarations for TestSpectra [${platform.toUpperCase()}]\n`;
331
+ content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
332
+ // TestSpectra's Rust-native orchestrator worker (core/orchestrator/src/worker/bdd_runner.ts)
333
+ // registers its own `it`/`test`/lifecycle-hook globals directly — there is no WDIO or Mocha
334
+ // runtime involved at all (not just for component testing; this is true across every
335
+ // platform). `describe` is intentionally NOT declared here even though the runtime has a
336
+ // transparent no-op implementation of it — spec-schema-linter's no-describe-block rule
337
+ // forbids authoring it, so leaving it undeclared makes that a compile error too, not just a
338
+ // lint error.
339
+ content += `declare function it(title: string, fn: () => Promise<void> | void): void;\n`;
340
+ if (isComponentTesting) {
341
+ // Vitest's own `expect` global (test.globals: true in tools/react/vitest.config.ts) — the
342
+ // one thing component specs legitimately need beyond the Spectra.* DSL, for asserting
343
+ // plain closure/callback state (e.g. "was this onClick handler invoked") that has no DOM
344
+ // element to target. Reuses @testspectra/react's own re-exported `ExpectStatic` type
345
+ // rather than hand-duplicating Vitest's expect API surface. Not declared for E2E projects
346
+ // — the Rust orchestrator's BDD runtime has no `expect` global at all.
347
+ content += `declare const expect: import('@testspectra/react').ExpectStatic;\n`;
348
+ }
349
+ content += `/// <reference types="@testspectra/matchers" />\n`;
350
+ content += `/// <reference path="./fixtures.d.ts" />\n`;
351
+ content += `/// <reference path="${envRefPath}" />\n\n`;
352
+ // 1. Page Objects Global Declarations
353
+ const declaredPOMs = new Set();
354
+ for (const poDir of poDirs) {
355
+ if (!fs.existsSync(poDir))
356
+ continue;
357
+ const poEntries = fs.readdirSync(poDir, { withFileTypes: true });
358
+ for (const entry of poEntries) {
359
+ if (entry.name.startsWith('.'))
360
+ continue;
361
+ if (entry.isDirectory()) {
362
+ const className = entry.name;
363
+ if (declaredPOMs.has(className))
364
+ continue;
365
+ const classFolder = path.join(poDir, className);
366
+ let matchedFile = null;
367
+ for (const stem of hierarchy) {
368
+ const candidates = [
369
+ path.join(classFolder, `${stem}.ts`),
370
+ path.join(classFolder, `${stem}.page.ts`),
371
+ path.join(classFolder, `${stem}.po.ts`),
372
+ ];
373
+ if (stem === 'common') {
374
+ candidates.push(path.join(classFolder, 'common.ts'));
375
+ candidates.push(path.join(classFolder, 'index.ts'));
376
+ }
377
+ for (const cand of candidates) {
378
+ if (fs.existsSync(cand)) {
379
+ matchedFile = path.relative(outputTypesDir, cand).replace(/\\/g, '/').replace(/\.ts$/, '.js');
380
+ if (!matchedFile.startsWith('.'))
381
+ matchedFile = `./${matchedFile}`;
382
+ break;
383
+ }
384
+ }
385
+ if (matchedFile)
386
+ break;
387
+ }
388
+ if (matchedFile) {
389
+ content += `declare const ${className}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
390
+ declaredPOMs.add(className);
391
+ }
392
+ }
393
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
394
+ const parts = entry.name.split('.');
395
+ const className = parts[0];
396
+ if (declaredPOMs.has(className))
397
+ continue;
398
+ let isMatch = false;
399
+ if (parts.length === 2) {
400
+ isMatch = true;
401
+ }
402
+ else if (parts.length > 2) {
403
+ const stem = parts[1];
404
+ isMatch = hierarchy.includes(stem);
405
+ }
406
+ if (isMatch) {
407
+ let rel = path
408
+ .relative(outputTypesDir, path.join(poDir, entry.name))
409
+ .replace(/\\/g, '/')
410
+ .replace(/\.ts$/, '.js');
411
+ if (!rel.startsWith('.'))
412
+ rel = `./${rel}`;
413
+ content += `declare const ${className}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
414
+ declaredPOMs.add(className);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ // 2. Built-in Spectra Static Methods & Custom Actions
420
+ content += `interface TestSpectraCustomActions {\n`;
421
+ const declaredActions = new Set();
422
+ for (const actionsDir of actionsDirs) {
423
+ if (!fs.existsSync(actionsDir))
424
+ continue;
425
+ const actionEntries = fs.readdirSync(actionsDir, { withFileTypes: true });
426
+ for (const entry of actionEntries) {
427
+ if (entry.name.startsWith('.'))
428
+ continue;
429
+ if (entry.isDirectory()) {
430
+ const actionName = entry.name;
431
+ if (declaredActions.has(actionName))
432
+ continue;
433
+ const actionFolder = path.join(actionsDir, actionName);
434
+ let matchedFile = null;
435
+ for (const stem of hierarchy) {
436
+ const cand1 = path.join(actionFolder, `${stem}.action.ts`);
437
+ const cand2 = path.join(actionFolder, `${stem}.ts`);
438
+ if (fs.existsSync(cand1)) {
439
+ matchedFile = path.relative(outputTypesDir, cand1).replace(/\\/g, '/').replace(/\.ts$/, '.js');
440
+ break;
441
+ }
442
+ else if (fs.existsSync(cand2)) {
443
+ matchedFile = path.relative(outputTypesDir, cand2).replace(/\\/g, '/').replace(/\.ts$/, '.js');
444
+ break;
445
+ }
446
+ }
447
+ if (matchedFile) {
448
+ if (!matchedFile.startsWith('.'))
449
+ matchedFile = `./${matchedFile}`;
450
+ const camel = actionName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
451
+ content += ` "${actionName}": (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
452
+ if (camel !== actionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
453
+ content += ` ${camel}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
454
+ }
455
+ declaredActions.add(actionName);
456
+ }
457
+ }
458
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
459
+ const parts = entry.name.split('.');
460
+ const actionName = parts[0];
461
+ let stem = 'common';
462
+ if (parts.length > 2) {
463
+ stem = parts[1];
464
+ }
465
+ else if (parts.length === 2) {
466
+ stem = 'common';
467
+ }
468
+ if (hierarchy.includes(stem)) {
469
+ if (declaredActions.has(actionName))
470
+ continue;
471
+ let rel = path
472
+ .relative(outputTypesDir, path.join(actionsDir, entry.name))
473
+ .replace(/\\/g, '/')
474
+ .replace(/\.ts$/, '.js');
475
+ if (!rel.startsWith('.'))
476
+ rel = `./${rel}`;
477
+ const camel = actionName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
478
+ content += ` "${actionName}": (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
479
+ if (camel !== actionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
480
+ content += ` ${camel}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
481
+ }
482
+ declaredActions.add(actionName);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ content += `}\n`;
488
+ // Reuses @testspectra/react's own exported `mount` type rather than hand-duplicating its
489
+ // signature here — stays in sync automatically if that signature ever changes.
490
+ const mountAugment = isComponentTesting ? ` & { mount: typeof import('@testspectra/react').mount }` : '';
491
+ content += `type TestSpectraActions = import('@testspectra/matchers').SpectraStatic & TestSpectraCustomActions${mountAugment};\n`;
492
+ content += `declare const Spectra: TestSpectraActions;\n\n`;
493
+ // 3. Built-in Steps Declaration
494
+ content += `interface TestSpectraSteps {\n`;
495
+ const declaredSteps = new Set();
496
+ for (const stepsDir of stepsDirs) {
497
+ if (!fs.existsSync(stepsDir))
498
+ continue;
499
+ const stepEntries = fs.readdirSync(stepsDir, { withFileTypes: true });
500
+ for (const entry of stepEntries) {
501
+ if (entry.name.startsWith('.'))
502
+ continue;
503
+ if (entry.isDirectory()) {
504
+ const stepName = entry.name;
505
+ if (declaredSteps.has(stepName))
506
+ continue;
507
+ const stepFolder = path.join(stepsDir, stepName);
508
+ let matchedFile = null;
509
+ for (const stem of hierarchy) {
510
+ const cand1 = path.join(stepFolder, `${stem}.step.ts`);
511
+ const cand2 = path.join(stepFolder, `${stem}.ts`);
512
+ if (fs.existsSync(cand1)) {
513
+ matchedFile = path.relative(outputTypesDir, cand1).replace(/\\/g, '/').replace(/\.ts$/, '.js');
514
+ break;
515
+ }
516
+ else if (fs.existsSync(cand2)) {
517
+ matchedFile = path.relative(outputTypesDir, cand2).replace(/\\/g, '/').replace(/\.ts$/, '.js');
518
+ break;
519
+ }
520
+ }
521
+ if (matchedFile) {
522
+ if (!matchedFile.startsWith('.'))
523
+ matchedFile = `./${matchedFile}`;
524
+ const camel = stepName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
525
+ content += ` "${stepName}": (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
526
+ if (camel !== stepName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
527
+ content += ` ${camel}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
528
+ }
529
+ declaredSteps.add(stepName);
530
+ }
531
+ }
532
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
533
+ const parts = entry.name.split('.');
534
+ const stepName = parts[0];
535
+ let stem = 'common';
536
+ if (parts.length > 2) {
537
+ stem = parts[1];
538
+ }
539
+ else if (parts.length === 2) {
540
+ stem = 'common';
541
+ }
542
+ if (hierarchy.includes(stem)) {
543
+ if (declaredSteps.has(stepName))
544
+ continue;
545
+ let rel = path
546
+ .relative(outputTypesDir, path.join(stepsDir, entry.name))
547
+ .replace(/\\/g, '/')
548
+ .replace(/\.ts$/, '.js');
549
+ if (!rel.startsWith('.'))
550
+ rel = `./${rel}`;
551
+ const camel = stepName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
552
+ content += ` "${stepName}": (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
553
+ if (camel !== stepName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
554
+ content += ` ${camel}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
555
+ }
556
+ declaredSteps.add(stepName);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ content += `}\n`;
562
+ content += `declare const Step: TestSpectraSteps;\n\n`;
563
+ results[platform] = content;
564
+ }
565
+ return results;
566
+ }
567
+ static generateFixturesDeclarationForScope(cwd, outputTypesDir, scopeDir, sharedDir) {
568
+ const fixturesDirs = this.getEntityDirsForScope('fixtures', scopeDir, sharedDir);
569
+ let content = `// Auto-generated ambient fixture declarations for TestSpectra\n`;
570
+ content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
571
+ content += `interface TestSpectraFixtures {\n`;
572
+ const declaredFixtures = new Set();
573
+ function scanFixtureDir(dir) {
574
+ if (!fs.existsSync(dir))
575
+ return;
576
+ try {
577
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
578
+ for (const entry of entries) {
579
+ if (entry.name.startsWith('.'))
580
+ continue;
581
+ if (entry.isDirectory()) {
582
+ scanFixtureDir(path.join(dir, entry.name));
583
+ }
584
+ else if (entry.isFile()) {
585
+ const parsed = path.parse(entry.name);
586
+ const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, '_');
587
+ if (declaredFixtures.has(varName))
588
+ continue;
589
+ if (entry.name.endsWith('.json')) {
590
+ let rel = path.relative(outputTypesDir, path.join(dir, entry.name)).replace(/\\/g, '/');
591
+ if (!rel.startsWith('.'))
592
+ rel = `./${rel}`;
593
+ content += ` readonly ${varName}: typeof import('${rel}');\n`;
594
+ }
595
+ else {
596
+ content += ` readonly ${varName}: string;\n`;
597
+ }
598
+ declaredFixtures.add(varName);
599
+ }
600
+ }
601
+ }
602
+ catch { }
603
+ }
604
+ for (const fixturesDir of fixturesDirs) {
605
+ scanFixtureDir(fixturesDir);
606
+ }
607
+ content += `}\n`;
608
+ content += `declare const Fixture: TestSpectraFixtures;\n`;
609
+ return content;
610
+ }
611
+ /**
612
+ * Generates the typed `Spectra.env` ambient declaration from `spectra.config.ts`'s
613
+ * `executionConfig.environmentVariables` — mirrors `generateFixturesDeclarationForScope`, but
614
+ * env vars come from a single project-level config rather than a scanned directory.
615
+ *
616
+ * Emits a bare global `interface SpectraEnv { readonly KEY: string; ... }`, which TypeScript
617
+ * interface-merges with the empty `SpectraEnv` shipped by `@testspectra/matchers` (the same
618
+ * mechanism `@types/node` uses for projects to augment `NodeJS.ProcessEnv`) — so `Spectra.env.KEY`
619
+ * resolves to `string`, never `string | undefined` like raw `process.env.KEY`.
620
+ */
621
+ static generateEnvDeclaration(environmentVariables) {
622
+ let content = `// Auto-generated ambient Spectra.env declarations for TestSpectra\n`;
623
+ content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
624
+ content += `interface SpectraEnv {\n`;
625
+ for (const key of Object.keys(environmentVariables)) {
626
+ content += ` readonly ${JSON.stringify(key)}: string;\n`;
627
+ }
628
+ content += `}\n`;
629
+ return content;
630
+ }
631
+ static writeDeclarationFiles(cwd) {
632
+ const rootTypesDir = path.join(cwd, '.testspectra', 'types');
633
+ if (!fs.existsSync(rootTypesDir)) {
634
+ fs.mkdirSync(rootTypesDir, { recursive: true });
635
+ }
636
+ // env.d.ts always lives at the root, regardless of Nx scopes — a single spectra.config.ts's
637
+ // executionConfig.environmentVariables applies workspace-wide (see generateEnvDeclaration).
638
+ // Reading spectra.config.ts itself is unavoidably async (ESM dynamic import), which would ripple
639
+ // an async signature through every caller of this long-standing sync method — instead, this only
640
+ // guarantees env.d.ts *exists* (so the reference below never points at a missing file) and
641
+ // leaves it accurate as of the last real sync. `syncEnvDeclaration` (called at the actual
642
+ // execution entry points: run/watch/sync-types) is what keeps its content up to date.
643
+ const envDtsPath = path.join(rootTypesDir, 'env.d.ts');
644
+ if (!fs.existsSync(envDtsPath)) {
645
+ fs.writeFileSync(envDtsPath, this.generateEnvDeclaration({}), 'utf-8');
646
+ }
647
+ const scopes = this.getEntityScopes(cwd);
648
+ const sharedScope = scopes.find((s) => s.isShared);
649
+ const featureScopes = scopes.filter((s) => !s.isShared);
650
+ const hasScopes = Boolean(sharedScope || featureScopes.length > 0);
651
+ const hasRootEntities = fs.existsSync(path.join(cwd, 'specs')) ||
652
+ fs.existsSync(path.join(cwd, 'page-objects')) ||
653
+ fs.existsSync(path.join(cwd, 'pageobjects')) ||
654
+ fs.existsSync(path.join(cwd, 'support')) ||
655
+ fs.existsSync(path.join(cwd, 'fixtures'));
656
+ // 1. Generate Global / Standalone Root Types ONLY when there are root entities or no sub-scopes
657
+ if (!hasScopes || hasRootEntities) {
658
+ const rootFixtures = this.generateFixturesDeclarationForScope(cwd, rootTypesDir, cwd);
659
+ fs.writeFileSync(path.join(rootTypesDir, 'fixtures.d.ts'), rootFixtures, 'utf-8');
660
+ const rootDeclarations = this.generateDeclarationsForScope(cwd, rootTypesDir, cwd, undefined, rootTypesDir);
661
+ for (const [platform, content] of Object.entries(rootDeclarations)) {
662
+ fs.writeFileSync(path.join(rootTypesDir, `${platform}.d.ts`), content, 'utf-8');
663
+ }
664
+ }
665
+ else {
666
+ // In a pure monorepo where entities are partitioned in shared/ and packages/,
667
+ // purge any root platform .d.ts files so types are not emitted outside shared & package folders
668
+ const rootPlatformFiles = ['web.d.ts', 'mobile.d.ts', 'android.d.ts', 'ios.d.ts', 'common.d.ts', 'fixtures.d.ts'];
669
+ for (const f of rootPlatformFiles) {
670
+ const p = path.join(rootTypesDir, f);
671
+ if (fs.existsSync(p)) {
672
+ try {
673
+ fs.unlinkSync(p);
674
+ }
675
+ catch { }
676
+ }
677
+ }
678
+ }
679
+ // 2. Generate Scoped Types for Shared Library: .testspectra/types/shared/
680
+ if (sharedScope) {
681
+ const sharedTypesDir = path.join(rootTypesDir, 'shared');
682
+ if (!fs.existsSync(sharedTypesDir))
683
+ fs.mkdirSync(sharedTypesDir, { recursive: true });
684
+ const sharedFixtures = this.generateFixturesDeclarationForScope(cwd, sharedTypesDir, sharedScope.dir);
685
+ fs.writeFileSync(path.join(sharedTypesDir, 'fixtures.d.ts'), sharedFixtures, 'utf-8');
686
+ const sharedDeclarations = this.generateDeclarationsForScope(cwd, sharedTypesDir, sharedScope.dir, undefined, rootTypesDir);
687
+ for (const [platform, content] of Object.entries(sharedDeclarations)) {
688
+ fs.writeFileSync(path.join(sharedTypesDir, `${platform}.d.ts`), content, 'utf-8');
689
+ }
690
+ }
691
+ else {
692
+ const sharedTypesDir = path.join(rootTypesDir, 'shared');
693
+ if (fs.existsSync(sharedTypesDir)) {
694
+ try {
695
+ fs.rmSync(sharedTypesDir, { recursive: true, force: true });
696
+ }
697
+ catch { }
698
+ }
699
+ }
700
+ // 3. Generate Scoped Types for Feature Modules: .testspectra/types/features/<featureName>/
701
+ const activeFeatureNames = new Set(featureScopes.map((f) => f.name));
702
+ for (const feature of featureScopes) {
703
+ const featureTypesDir = path.join(rootTypesDir, 'features', feature.name);
704
+ if (!fs.existsSync(featureTypesDir))
705
+ fs.mkdirSync(featureTypesDir, { recursive: true });
706
+ const featureFixtures = this.generateFixturesDeclarationForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
707
+ fs.writeFileSync(path.join(featureTypesDir, 'fixtures.d.ts'), featureFixtures, 'utf-8');
708
+ const featureDeclarations = this.generateDeclarationsForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir, rootTypesDir);
709
+ for (const [platform, content] of Object.entries(featureDeclarations)) {
710
+ fs.writeFileSync(path.join(featureTypesDir, `${platform}.d.ts`), content, 'utf-8');
711
+ }
712
+ }
713
+ // Clean up stale feature folders
714
+ const featuresBaseDir = path.join(rootTypesDir, 'features');
715
+ if (fs.existsSync(featuresBaseDir)) {
716
+ try {
717
+ const existingFeatureDirs = fs.readdirSync(featuresBaseDir, { withFileTypes: true });
718
+ for (const entry of existingFeatureDirs) {
719
+ if (entry.isDirectory() && !activeFeatureNames.has(entry.name)) {
720
+ fs.rmSync(path.join(featuresBaseDir, entry.name), { recursive: true, force: true });
721
+ }
722
+ }
723
+ if (activeFeatureNames.size === 0) {
724
+ fs.rmSync(featuresBaseDir, { recursive: true, force: true });
725
+ }
726
+ }
727
+ catch { }
728
+ }
729
+ // Universal ambient index
730
+ const universalPath = path.join(rootTypesDir, 'index.d.ts');
731
+ if (sharedScope) {
732
+ fs.writeFileSync(universalPath, `/// <reference path="./shared/common.d.ts" />\n`, 'utf-8');
733
+ }
734
+ else {
735
+ fs.writeFileSync(universalPath, `/// <reference path="./common.d.ts" />\n`, 'utf-8');
736
+ }
737
+ // 4. Generate Scoped tsconfig solutions in .testspectra/tsconfig/
738
+ TsConfigGenerator.writeConfigs(cwd, scopes);
739
+ }
740
+ }