@testspectra/cli 1.0.70 → 1.1.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 (277) 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-DPfv5_EF.js +116 -0
  7. package/demo-app/assets/index-gIwu2EMV.css +1 -0
  8. package/demo-app/index.html +2 -2
  9. package/dist/.tsbuildinfo +1 -0
  10. package/dist/commands/__tests__/cloud-run.test.d.ts +1 -0
  11. package/dist/commands/__tests__/cloud-run.test.js +93 -0
  12. package/dist/commands/__tests__/doctor.test.d.ts +1 -0
  13. package/dist/commands/__tests__/doctor.test.js +39 -0
  14. package/dist/commands/__tests__/init.test.d.ts +1 -0
  15. package/dist/commands/__tests__/init.test.js +148 -0
  16. package/dist/commands/__tests__/license.test.d.ts +1 -0
  17. package/dist/commands/__tests__/license.test.js +13 -0
  18. package/dist/commands/__tests__/refactor.test.d.ts +1 -0
  19. package/dist/commands/__tests__/refactor.test.js +64 -0
  20. package/dist/commands/__tests__/run-e2e.test.d.ts +1 -0
  21. package/dist/commands/__tests__/run-e2e.test.js +181 -0
  22. package/dist/commands/__tests__/run.test.d.ts +1 -0
  23. package/dist/commands/__tests__/run.test.js +197 -0
  24. package/dist/commands/__tests__/watch.test.d.ts +1 -0
  25. package/dist/commands/__tests__/watch.test.js +155 -0
  26. package/dist/commands/add.js +86 -85
  27. package/dist/commands/cloud-run.d.ts +11 -0
  28. package/dist/commands/cloud-run.js +118 -0
  29. package/dist/commands/demo.js +7 -7
  30. package/dist/commands/devices.d.ts +14 -2
  31. package/dist/commands/devices.js +44 -34
  32. package/dist/commands/doctor.d.ts +3 -1
  33. package/dist/commands/doctor.js +43 -264
  34. package/dist/commands/init.d.ts +1 -1
  35. package/dist/commands/init.js +206 -196
  36. package/dist/commands/install.d.ts +4 -0
  37. package/dist/commands/install.js +59 -0
  38. package/dist/commands/license.d.ts +4 -0
  39. package/dist/commands/license.js +30 -0
  40. package/dist/commands/refactor.d.ts +8 -0
  41. package/dist/commands/refactor.js +78 -0
  42. package/dist/commands/run.d.ts +22 -1
  43. package/dist/commands/run.js +252 -109
  44. package/dist/commands/sync-types.d.ts +4 -0
  45. package/dist/commands/sync-types.js +14 -0
  46. package/dist/commands/update.js +39 -43
  47. package/dist/commands/watch.d.ts +6 -0
  48. package/dist/commands/watch.js +47 -43
  49. package/dist/config/loader.d.ts +1 -1
  50. package/dist/config/loader.js +40 -25
  51. package/dist/config/schema.d.ts +117 -49
  52. package/dist/config/schema.js +41 -34
  53. package/dist/generator/__tests__/tsconfig-isolation.test.d.ts +1 -0
  54. package/dist/generator/__tests__/tsconfig-isolation.test.js +278 -0
  55. package/dist/generator/__tests__/type-generator.test.d.ts +1 -0
  56. package/dist/generator/__tests__/type-generator.test.js +386 -0
  57. package/dist/generator/index.d.ts +2 -0
  58. package/dist/generator/index.js +2 -0
  59. package/dist/generator/tsconfig-generator.d.ts +27 -0
  60. package/dist/generator/tsconfig-generator.js +391 -0
  61. package/dist/generator/type-generator.d.ts +36 -0
  62. package/dist/generator/type-generator.js +644 -0
  63. package/dist/index.d.ts +9 -5
  64. package/dist/index.js +124 -51
  65. package/dist/lifecycle/__tests__/lifecycle-engine.test.d.ts +1 -0
  66. package/dist/lifecycle/__tests__/lifecycle-engine.test.js +290 -0
  67. package/dist/lifecycle/hook-discovery.d.ts +24 -0
  68. package/dist/lifecycle/hook-discovery.js +60 -0
  69. package/dist/lifecycle/hook-dispatcher.d.ts +34 -0
  70. package/dist/lifecycle/hook-dispatcher.js +190 -0
  71. package/dist/lifecycle/index.d.ts +3 -0
  72. package/dist/lifecycle/index.js +3 -0
  73. package/dist/lifecycle/parallel-grouping.d.ts +27 -0
  74. package/dist/lifecycle/parallel-grouping.js +151 -0
  75. package/dist/mobile/__tests__/android-device-e2e.test.d.ts +1 -0
  76. package/dist/mobile/__tests__/android-device-e2e.test.js +53 -0
  77. package/dist/mobile/__tests__/device-pool.test.d.ts +1 -0
  78. package/dist/mobile/__tests__/device-pool.test.js +97 -0
  79. package/dist/mobile/device-pool.d.ts +23 -0
  80. package/dist/mobile/device-pool.js +1 -0
  81. package/dist/mobile/index.d.ts +1 -0
  82. package/dist/mobile/index.js +1 -0
  83. package/dist/refactor/__tests__/refactor-engine.test.d.ts +1 -0
  84. package/dist/refactor/__tests__/refactor-engine.test.js +175 -0
  85. package/dist/refactor/index.d.ts +1 -0
  86. package/dist/refactor/index.js +1 -0
  87. package/dist/refactor/refactor-engine.d.ts +25 -0
  88. package/dist/refactor/refactor-engine.js +255 -0
  89. package/dist/reporter/__tests__/semantic-formatter.test.d.ts +1 -0
  90. package/dist/reporter/__tests__/semantic-formatter.test.js +73 -0
  91. package/dist/reporter/semantic-formatter.d.ts +6 -0
  92. package/dist/reporter/semantic-formatter.js +191 -0
  93. package/dist/reporter/summary-view.d.ts +12 -0
  94. package/dist/reporter/summary-view.js +19 -0
  95. package/dist/reporter/terminal-stream.d.ts +32 -0
  96. package/dist/reporter/terminal-stream.js +82 -0
  97. package/dist/reporter/types.d.ts +2 -0
  98. package/dist/reporter/types.js +1 -0
  99. package/dist/runner/bridge.d.ts +10 -5
  100. package/dist/runner/bridge.js +69 -44
  101. package/dist/runner/reporter.d.ts +30 -2
  102. package/dist/runner/reporter.js +419 -13
  103. package/dist/server/demo.js +43 -44
  104. package/dist/types/generator.d.ts +1 -17
  105. package/dist/types/generator.js +1 -443
  106. package/dist/utils/demo-server.js +38 -35
  107. package/package.json +20 -12
  108. package/templates/default/CLAUDE.md +7 -5
  109. package/templates/default/fixtures/apiData.json +24 -0
  110. package/templates/default/package.json +13 -4
  111. package/templates/default/page-objects/MatchersPage/mobile.ts +41 -16
  112. package/templates/default/page-objects/MatchersPage/web.ts +14 -0
  113. package/templates/default/page-objects/NavigationPage/mobile.ts +10 -1
  114. package/templates/default/page-objects/PlaygroundPage/mobile.ts +27 -17
  115. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -0
  116. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +11 -0
  117. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  118. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  119. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +16 -0
  120. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  121. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  122. package/templates/default/specs/ApiInterception/suite.md +9 -0
  123. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +7 -1
  124. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  125. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +15 -1
  126. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  127. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +9 -1
  128. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  129. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +9 -1
  130. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  131. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +27 -1
  132. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  133. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +4 -1
  134. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  135. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +4 -1
  136. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  137. package/templates/default/spectra.config.ts +21 -14
  138. package/templates/default/support/actions/fillCredentials/mobile.action.ts +2 -2
  139. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +2 -0
  140. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  141. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +3 -1
  142. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  143. package/templates/default/tsconfig.json +3 -1
  144. package/templates/nx/.nx/workspace-data/d/daemon.log +168 -0
  145. package/templates/nx/CLAUDE.md +2 -2
  146. package/templates/nx/package.json +7 -5
  147. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +3 -0
  148. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +12 -0
  149. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +27 -0
  150. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +3 -0
  151. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/spec.md +12 -0
  152. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +24 -0
  153. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/suite.md +9 -0
  154. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +17 -3
  155. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +14 -4
  156. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +8 -3
  157. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +9 -3
  158. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +27 -6
  159. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +3 -3
  160. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +2 -2
  161. package/templates/nx/pnpm-lock.yaml +6663 -0
  162. package/templates/nx/pnpm-workspace.yaml +2 -1
  163. package/templates/nx/shared/testing/fixtures/apiData.json +24 -0
  164. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +12 -0
  165. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +14 -0
  166. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -0
  167. package/templates/nx/shared/testing/project.json +19 -0
  168. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +2 -2
  169. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  170. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +3 -3
  171. package/templates/nx/spectra.config.ts +19 -12
  172. package/templates/nx/tsconfig.json +3 -1
  173. package/demo-app/assets/index-DAUlrHTr.css +0 -1
  174. package/demo-app/assets/index-DN2Kb0A8.js +0 -100
  175. package/dist/index.cjs +0 -101
  176. package/dist/plugin.cjs +0 -575
  177. package/dist/plugin.d.ts +0 -16
  178. package/dist/plugin.js +0 -184
  179. package/dist/step/index.d.ts +0 -6
  180. package/dist/step/index.js +0 -6
  181. package/dist/step/matchers.d.ts +0 -14
  182. package/dist/step/matchers.js +0 -114
  183. package/dist/step/proto.d.ts +0 -46
  184. package/dist/step/proto.js +0 -114
  185. package/dist/step/runner/collection.d.ts +0 -13
  186. package/dist/step/runner/collection.js +0 -51
  187. package/dist/step/runner/single.d.ts +0 -21
  188. package/dist/step/runner/single.js +0 -138
  189. package/dist/step/spectra.d.ts +0 -34
  190. package/dist/step/spectra.js +0 -156
  191. package/dist/step/types.d.ts +0 -32
  192. package/dist/step/types.js +0 -5
  193. package/templates/default/tsconfig.spectra.android.json +0 -46
  194. package/templates/default/tsconfig.spectra.ios.json +0 -46
  195. package/templates/default/tsconfig.spectra.json +0 -22
  196. package/templates/default/tsconfig.spectra.web.json +0 -53
  197. package/templates/nx/fixtures/playgroundData.json +0 -7
  198. package/templates/nx/global-hooks/after/mobile.hook.ts +0 -3
  199. package/templates/nx/global-hooks/after/web.hook.ts +0 -3
  200. package/templates/nx/global-hooks/before/mobile.hook.ts +0 -3
  201. package/templates/nx/global-hooks/before/web.hook.ts +0 -3
  202. package/templates/nx/page-objects/MatchersPage/mobile.ts +0 -21
  203. package/templates/nx/page-objects/MatchersPage/web.ts +0 -23
  204. package/templates/nx/page-objects/NavigationPage/mobile.ts +0 -5
  205. package/templates/nx/page-objects/NavigationPage/web.ts +0 -8
  206. package/templates/nx/page-objects/PlaygroundPage/mobile.ts +0 -26
  207. package/templates/nx/page-objects/PlaygroundPage/web.ts +0 -29
  208. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +0 -3
  209. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +0 -12
  210. package/templates/nx/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +0 -5
  211. package/templates/nx/specs/BrowserContext/suite.md +0 -9
  212. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -3
  213. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/spec.md +0 -12
  214. package/templates/nx/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -7
  215. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +0 -3
  216. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +0 -12
  217. package/templates/nx/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +0 -6
  218. package/templates/nx/specs/ContentMatchers/suite.md +0 -9
  219. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +0 -3
  220. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +0 -12
  221. package/templates/nx/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +0 -5
  222. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +0 -3
  223. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +0 -12
  224. package/templates/nx/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +0 -8
  225. package/templates/nx/specs/ElementMatchers/hooks/after/mobile.hook.ts +0 -1
  226. package/templates/nx/specs/ElementMatchers/hooks/after/web.hook.ts +0 -1
  227. package/templates/nx/specs/ElementMatchers/hooks/afterEach/mobile.hook.ts +0 -1
  228. package/templates/nx/specs/ElementMatchers/hooks/afterEach/web.hook.ts +0 -1
  229. package/templates/nx/specs/ElementMatchers/hooks/before/mobile.hook.ts +0 -1
  230. package/templates/nx/specs/ElementMatchers/hooks/before/web.hook.ts +0 -1
  231. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/mobile.hook.ts +0 -1
  232. package/templates/nx/specs/ElementMatchers/hooks/beforeEach/web.hook.ts +0 -1
  233. package/templates/nx/specs/ElementMatchers/suite.md +0 -9
  234. package/templates/nx/specs/Playground/TC-0001-form-controls/mobile.test.ts +0 -3
  235. package/templates/nx/specs/Playground/TC-0001-form-controls/spec.md +0 -12
  236. package/templates/nx/specs/Playground/TC-0001-form-controls/web.test.ts +0 -6
  237. package/templates/nx/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +0 -3
  238. package/templates/nx/specs/Playground/TC-0002-toggle-elements/spec.md +0 -12
  239. package/templates/nx/specs/Playground/TC-0002-toggle-elements/web.test.ts +0 -6
  240. package/templates/nx/specs/Playground/hooks/after/mobile.hook.ts +0 -1
  241. package/templates/nx/specs/Playground/hooks/after/web.hook.ts +0 -1
  242. package/templates/nx/specs/Playground/hooks/afterEach/mobile.hook.ts +0 -1
  243. package/templates/nx/specs/Playground/hooks/afterEach/web.hook.ts +0 -1
  244. package/templates/nx/specs/Playground/hooks/before/mobile.hook.ts +0 -1
  245. package/templates/nx/specs/Playground/hooks/before/web.hook.ts +0 -1
  246. package/templates/nx/specs/Playground/hooks/beforeEach/mobile.hook.ts +0 -1
  247. package/templates/nx/specs/Playground/hooks/beforeEach/web.hook.ts +0 -1
  248. package/templates/nx/specs/Playground/suite.md +0 -9
  249. package/templates/nx/support/actions/fillCredentials/mobile.action.ts +0 -4
  250. package/templates/nx/support/actions/fillCredentials/web.action.ts +0 -4
  251. package/templates/nx/support/steps/togglePlaygroundStates/mobile.step.ts +0 -3
  252. package/templates/nx/support/steps/togglePlaygroundStates/step.md +0 -7
  253. package/templates/nx/support/steps/togglePlaygroundStates/web.step.ts +0 -5
  254. package/templates/nx/support/steps/verifyPlaygroundState/mobile.step.ts +0 -4
  255. package/templates/nx/support/steps/verifyPlaygroundState/step.md +0 -7
  256. package/templates/nx/support/steps/verifyPlaygroundState/web.step.ts +0 -6
  257. package/templates/nx/tsconfig.spectra.android.json +0 -39
  258. package/templates/nx/tsconfig.spectra.ios.json +0 -39
  259. package/templates/nx/tsconfig.spectra.json +0 -25
  260. package/templates/nx/tsconfig.spectra.shared.json +0 -25
  261. package/templates/nx/tsconfig.spectra.web.json +0 -45
  262. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  263. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  264. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  265. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  266. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  267. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  268. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  269. /package/templates/{nx/specs/BrowserContext → default/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
  270. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/mobile.hook.ts +0 -0
  271. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/after/web.hook.ts +0 -0
  272. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/mobile.hook.ts +0 -0
  273. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/afterEach/web.hook.ts +0 -0
  274. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/mobile.hook.ts +0 -0
  275. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/before/web.hook.ts +0 -0
  276. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/mobile.hook.ts +0 -0
  277. /package/templates/nx/{specs/ContentMatchers → packages/matchers/e2e/specs/ApiInterception}/hooks/beforeEach/web.hook.ts +0 -0
@@ -0,0 +1,644 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { TsConfigGenerator } from './tsconfig-generator.js';
4
+ export const PLATFORM_HIERARCHY = {
5
+ web: ['web', 'common'],
6
+ android: ['android', 'mobile', 'common'],
7
+ ios: ['ios', 'mobile', 'common'],
8
+ mobile: ['mobile', 'common'],
9
+ common: ['common'],
10
+ };
11
+ /**
12
+ * Discovers all feature directories and shared library directories.
13
+ * Standard discovery hierarchy:
14
+ * 1. Nx / Monorepo Project Descriptors (`project.json`):
15
+ * - projectType: "library" OR tags: ["type:shared", "type:library", "scope:shared"] => Shared Library Scope (isShared: true)
16
+ * - projectType: "application" OR tags: ["type:e2e", "type:app"] => Feature Scope (isShared: false)
17
+ * 2. Explicit User Configuration (`tsconfig.spectra.shared.json`):
18
+ * - Custom include path for shared testing utilities
19
+ * 3. Dynamic Workspace Filesystem Discovery (Fallback for non-Nx / custom layouts):
20
+ * - Directory with specs/e2e/tests subfolders => Feature Scope
21
+ * - Directory with page-objects/support/fixtures and NO specs => Shared Scope
22
+ */
23
+ export function discoverEntityScopes(cwd) {
24
+ const scopes = [];
25
+ const ignoredDirNames = new Set([
26
+ 'node_modules',
27
+ 'dist',
28
+ '.git',
29
+ '.testspectra',
30
+ 'templates',
31
+ 'src',
32
+ 'build',
33
+ 'coverage',
34
+ 'scripts',
35
+ '.nx',
36
+ '.turbo',
37
+ '.vscode',
38
+ ]);
39
+ // Priority 1: Scan for Nx `project.json` files throughout the workspace
40
+ function scanForNxProjects(dir, depth) {
41
+ if (depth <= 0 || !fs.existsSync(dir))
42
+ return;
43
+ const base = path.basename(dir);
44
+ if (ignoredDirNames.has(base))
45
+ return;
46
+ const projectJsonPath = path.join(dir, 'project.json');
47
+ if (fs.existsSync(projectJsonPath)) {
48
+ try {
49
+ const pObj = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
50
+ const projectType = pObj.projectType;
51
+ const tags = Array.isArray(pObj.tags) ? pObj.tags : [];
52
+ const isSharedLib = projectType === 'library' ||
53
+ tags.includes('type:shared') ||
54
+ tags.includes('type:library') ||
55
+ tags.includes('scope:shared');
56
+ const isE2eApp = projectType === 'application' ||
57
+ tags.includes('type:e2e') ||
58
+ tags.includes('type:app') ||
59
+ tags.some((t) => t.startsWith('scope:') && t !== 'scope:shared');
60
+ if (isSharedLib) {
61
+ if (!scopes.some((s) => s.dir === dir)) {
62
+ scopes.push({ name: 'shared', dir, isShared: true });
63
+ }
64
+ return; // Do not recurse into subdirectories of an identified project
65
+ }
66
+ else if (isE2eApp) {
67
+ // Extract scope name from tags (e.g. "scope:matchers" => "matchers") or project name
68
+ const scopeTag = tags.find((t) => t.startsWith('scope:') && t !== 'scope:shared');
69
+ let featureName = scopeTag ? scopeTag.replace('scope:', '') : '';
70
+ if (!featureName && pObj.name) {
71
+ featureName = pObj.name.replace(/-e2e$/, '').replace(/^e2e-/, '');
72
+ }
73
+ if (!featureName) {
74
+ featureName = path.basename(base === 'e2e' ? path.dirname(dir) : dir);
75
+ }
76
+ if (!scopes.some((s) => s.dir === dir || s.name === featureName)) {
77
+ scopes.push({ name: featureName, dir, isShared: false });
78
+ }
79
+ return;
80
+ }
81
+ }
82
+ catch { }
83
+ }
84
+ try {
85
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
86
+ for (const entry of entries) {
87
+ if (entry.isDirectory()) {
88
+ scanForNxProjects(path.join(dir, entry.name), depth - 1);
89
+ }
90
+ }
91
+ }
92
+ catch { }
93
+ }
94
+ scanForNxProjects(cwd, 5);
95
+ // If projects were discovered via project.json, we have explicit deterministic descriptors
96
+ if (scopes.length > 0) {
97
+ return scopes;
98
+ }
99
+ // Priority 2: Check if user-provided tsconfig.spectra.shared.json defines custom shared library paths
100
+ const userSharedConfig = path.join(cwd, 'tsconfig.spectra.shared.json');
101
+ if (fs.existsSync(userSharedConfig)) {
102
+ try {
103
+ const conf = JSON.parse(fs.readFileSync(userSharedConfig, 'utf-8'));
104
+ if (Array.isArray(conf.include)) {
105
+ for (const inc of conf.include) {
106
+ if (typeof inc === 'string' && inc.includes('**')) {
107
+ const baseCandidate = inc.split('/**')[0];
108
+ const resolved = path.resolve(cwd, baseCandidate);
109
+ if (fs.existsSync(resolved) && !scopes.some((s) => s.dir === resolved)) {
110
+ scopes.push({ name: 'shared', dir: resolved, isShared: true });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ catch { }
117
+ }
118
+ // Priority 3: Fallback Directory Scan (for non-Nx or custom workspaces)
119
+ const entityDirNames = new Set(['page-objects', 'pageobjects', 'support', 'fixtures', 'global-hooks']);
120
+ function hasTestingEntities(dir) {
121
+ if (!fs.existsSync(dir))
122
+ return false;
123
+ const entityNames = ['page-objects', 'pageobjects', 'support', 'steps', 'actions', 'fixtures'];
124
+ return entityNames.some((name) => fs.existsSync(path.join(dir, name)));
125
+ }
126
+ function hasTestingSpecs(dir) {
127
+ if (!fs.existsSync(dir))
128
+ return false;
129
+ const specNames = ['specs', 'e2e', 'tests'];
130
+ return specNames.some((name) => fs.existsSync(path.join(dir, name)));
131
+ }
132
+ function fallbackScan(dir, depth) {
133
+ if (depth <= 0 || !fs.existsSync(dir))
134
+ return;
135
+ const base = path.basename(dir);
136
+ if (ignoredDirNames.has(base) || entityDirNames.has(base))
137
+ return;
138
+ if (dir !== cwd) {
139
+ if (base === 'e2e' || base === 'specs' || base === 'tests') {
140
+ const parentDir = path.dirname(dir);
141
+ if (parentDir !== cwd) {
142
+ const featureName = path.basename(parentDir);
143
+ if (!scopes.some((s) => s.dir === dir)) {
144
+ scopes.push({ name: featureName, dir, isShared: false });
145
+ }
146
+ return;
147
+ }
148
+ }
149
+ if (hasTestingSpecs(dir)) {
150
+ let targetDir = dir;
151
+ if (fs.existsSync(path.join(dir, 'e2e')))
152
+ targetDir = path.join(dir, 'e2e');
153
+ else if (fs.existsSync(path.join(dir, 'specs')))
154
+ targetDir = path.join(dir, 'specs');
155
+ else if (fs.existsSync(path.join(dir, 'tests')))
156
+ targetDir = path.join(dir, 'tests');
157
+ const featureName = path.basename(dir);
158
+ if (!scopes.some((s) => s.dir === targetDir || s.name === featureName)) {
159
+ scopes.push({ name: featureName, dir: targetDir, isShared: false });
160
+ }
161
+ return;
162
+ }
163
+ if (hasTestingEntities(dir) && !hasTestingSpecs(dir)) {
164
+ if (!scopes.some((s) => s.isShared && s.dir === dir)) {
165
+ scopes.push({ name: 'shared', dir, isShared: true });
166
+ return;
167
+ }
168
+ }
169
+ }
170
+ try {
171
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
172
+ for (const entry of entries) {
173
+ if (entry.isDirectory()) {
174
+ fallbackScan(path.join(dir, entry.name), depth - 1);
175
+ }
176
+ }
177
+ }
178
+ catch { }
179
+ }
180
+ fallbackScan(cwd, 5);
181
+ return scopes;
182
+ }
183
+ export class TypeGenerator {
184
+ /**
185
+ * Main entry point to generate all ambient type declarations.
186
+ */
187
+ static generateAll(cwd) {
188
+ this.writeDeclarationFiles(cwd);
189
+ TsConfigGenerator.scaffoldConfigs(cwd);
190
+ }
191
+ /**
192
+ * Discovers all feature directories and shared library directories.
193
+ * Returns a map of scope -> base directory
194
+ */
195
+ static getEntityScopes(cwd) {
196
+ return discoverEntityScopes(cwd);
197
+ }
198
+ static getPageObjectsDirsForScope(scopeDir, sharedDir) {
199
+ const dirs = [];
200
+ function collectPo(dir) {
201
+ if (!fs.existsSync(dir))
202
+ return;
203
+ const direct = [path.join(dir, 'page-objects'), path.join(dir, 'pageobjects')];
204
+ for (const d of direct) {
205
+ if (fs.existsSync(d) && !dirs.includes(d))
206
+ dirs.push(d);
207
+ }
208
+ try {
209
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
210
+ for (const entry of entries) {
211
+ if (entry.isDirectory() &&
212
+ entry.name !== 'node_modules' &&
213
+ entry.name !== 'dist' &&
214
+ entry.name !== '.git' &&
215
+ entry.name !== '.testspectra') {
216
+ const sub = [path.join(dir, entry.name, 'page-objects'), path.join(dir, entry.name, 'pageobjects')];
217
+ for (const s of sub) {
218
+ if (fs.existsSync(s) && !dirs.includes(s))
219
+ dirs.push(s);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ catch { }
225
+ }
226
+ collectPo(scopeDir);
227
+ if (sharedDir && sharedDir !== scopeDir) {
228
+ collectPo(sharedDir);
229
+ }
230
+ return dirs;
231
+ }
232
+ static getEntityDirsForScope(entityType, scopeDir, sharedDir) {
233
+ const dirs = [];
234
+ function collectEntities(dir) {
235
+ if (!fs.existsSync(dir))
236
+ return;
237
+ const direct = [
238
+ path.join(dir, entityType),
239
+ path.join(dir, 'support', entityType),
240
+ path.join(dir, 'support', 'testing', entityType),
241
+ ];
242
+ for (const d of direct) {
243
+ if (fs.existsSync(d) && !dirs.includes(d))
244
+ dirs.push(d);
245
+ }
246
+ try {
247
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
248
+ for (const entry of entries) {
249
+ if (entry.isDirectory() &&
250
+ entry.name !== 'node_modules' &&
251
+ entry.name !== 'dist' &&
252
+ entry.name !== '.git' &&
253
+ entry.name !== '.testspectra') {
254
+ const sub = [path.join(dir, entry.name, entityType), path.join(dir, entry.name, 'support', entityType)];
255
+ for (const s of sub) {
256
+ if (fs.existsSync(s) && !dirs.includes(s))
257
+ dirs.push(s);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ catch { }
263
+ }
264
+ collectEntities(scopeDir);
265
+ if (sharedDir && sharedDir !== scopeDir) {
266
+ collectEntities(sharedDir);
267
+ }
268
+ return dirs;
269
+ }
270
+ static generateDeclarationsForScope(cwd, outputTypesDir, scopeDir, sharedDir) {
271
+ const poDirs = this.getPageObjectsDirsForScope(scopeDir, sharedDir);
272
+ const actionsDirs = this.getEntityDirsForScope('actions', scopeDir, sharedDir);
273
+ const stepsDirs = this.getEntityDirsForScope('steps', scopeDir, sharedDir);
274
+ const platforms = ['web', 'android', 'ios', 'mobile', 'common'];
275
+ const results = {
276
+ web: '',
277
+ android: '',
278
+ ios: '',
279
+ mobile: '',
280
+ common: '',
281
+ };
282
+ for (const platform of platforms) {
283
+ const hierarchy = PLATFORM_HIERARCHY[platform];
284
+ let content = `// Auto-generated ambient declarations for TestSpectra [${platform.toUpperCase()}]\n`;
285
+ content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
286
+ content += `/// <reference types="@wdio/globals/types" />\n`;
287
+ content += `/// <reference types="@wdio/mocha-framework" />\n`;
288
+ content += `/// <reference types="@testspectra/matchers" />\n`;
289
+ content += `/// <reference path="./fixtures.d.ts" />\n\n`;
290
+ // 1. Page Objects Global Declarations
291
+ const declaredPOMs = new Set();
292
+ for (const poDir of poDirs) {
293
+ if (!fs.existsSync(poDir))
294
+ continue;
295
+ const poEntries = fs.readdirSync(poDir, { withFileTypes: true });
296
+ for (const entry of poEntries) {
297
+ if (entry.name.startsWith('.'))
298
+ continue;
299
+ if (entry.isDirectory()) {
300
+ const className = entry.name;
301
+ if (declaredPOMs.has(className))
302
+ continue;
303
+ const classFolder = path.join(poDir, className);
304
+ let matchedFile = null;
305
+ for (const stem of hierarchy) {
306
+ const candidates = [
307
+ path.join(classFolder, `${stem}.ts`),
308
+ path.join(classFolder, `${stem}.page.ts`),
309
+ path.join(classFolder, `${stem}.po.ts`),
310
+ ];
311
+ if (stem === 'common') {
312
+ candidates.push(path.join(classFolder, 'common.ts'));
313
+ candidates.push(path.join(classFolder, 'index.ts'));
314
+ }
315
+ for (const cand of candidates) {
316
+ if (fs.existsSync(cand)) {
317
+ matchedFile = path.relative(outputTypesDir, cand).replace(/\\/g, '/').replace(/\.ts$/, '.js');
318
+ if (!matchedFile.startsWith('.'))
319
+ matchedFile = `./${matchedFile}`;
320
+ break;
321
+ }
322
+ }
323
+ if (matchedFile)
324
+ break;
325
+ }
326
+ if (matchedFile) {
327
+ content += `declare const ${className}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
328
+ declaredPOMs.add(className);
329
+ }
330
+ }
331
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
332
+ const parts = entry.name.split('.');
333
+ const className = parts[0];
334
+ if (declaredPOMs.has(className))
335
+ continue;
336
+ let isMatch = false;
337
+ if (parts.length === 2) {
338
+ isMatch = true;
339
+ }
340
+ else if (parts.length > 2) {
341
+ const stem = parts[1];
342
+ isMatch = hierarchy.includes(stem);
343
+ }
344
+ if (isMatch) {
345
+ let rel = path
346
+ .relative(outputTypesDir, path.join(poDir, entry.name))
347
+ .replace(/\\/g, '/')
348
+ .replace(/\.ts$/, '.js');
349
+ if (!rel.startsWith('.'))
350
+ rel = `./${rel}`;
351
+ content += `declare const ${className}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
352
+ declaredPOMs.add(className);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ // 2. Built-in Spectra Static Methods & Custom Actions
358
+ content += `interface TestSpectraCustomActions {\n`;
359
+ const declaredActions = new Set();
360
+ for (const actionsDir of actionsDirs) {
361
+ if (!fs.existsSync(actionsDir))
362
+ continue;
363
+ const actionEntries = fs.readdirSync(actionsDir, { withFileTypes: true });
364
+ for (const entry of actionEntries) {
365
+ if (entry.name.startsWith('.'))
366
+ continue;
367
+ if (entry.isDirectory()) {
368
+ const actionName = entry.name;
369
+ if (declaredActions.has(actionName))
370
+ continue;
371
+ const actionFolder = path.join(actionsDir, actionName);
372
+ let matchedFile = null;
373
+ for (const stem of hierarchy) {
374
+ const cand1 = path.join(actionFolder, `${stem}.action.ts`);
375
+ const cand2 = path.join(actionFolder, `${stem}.ts`);
376
+ if (fs.existsSync(cand1)) {
377
+ matchedFile = path.relative(outputTypesDir, cand1).replace(/\\/g, '/').replace(/\.ts$/, '.js');
378
+ break;
379
+ }
380
+ else if (fs.existsSync(cand2)) {
381
+ matchedFile = path.relative(outputTypesDir, cand2).replace(/\\/g, '/').replace(/\.ts$/, '.js');
382
+ break;
383
+ }
384
+ }
385
+ if (matchedFile) {
386
+ if (!matchedFile.startsWith('.'))
387
+ matchedFile = `./${matchedFile}`;
388
+ const camel = actionName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
389
+ content += ` "${actionName}": (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
390
+ if (camel !== actionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
391
+ content += ` ${camel}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
392
+ }
393
+ declaredActions.add(actionName);
394
+ }
395
+ }
396
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
397
+ const parts = entry.name.split('.');
398
+ const actionName = parts[0];
399
+ let stem = 'common';
400
+ if (parts.length > 2) {
401
+ stem = parts[1];
402
+ }
403
+ else if (parts.length === 2) {
404
+ stem = 'common';
405
+ }
406
+ if (hierarchy.includes(stem)) {
407
+ if (declaredActions.has(actionName))
408
+ continue;
409
+ let rel = path
410
+ .relative(outputTypesDir, path.join(actionsDir, entry.name))
411
+ .replace(/\\/g, '/')
412
+ .replace(/\.ts$/, '.js');
413
+ if (!rel.startsWith('.'))
414
+ rel = `./${rel}`;
415
+ const camel = actionName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
416
+ content += ` "${actionName}": (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
417
+ if (camel !== actionName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
418
+ content += ` ${camel}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
419
+ }
420
+ declaredActions.add(actionName);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ content += `}\n`;
426
+ content += `type TestSpectraActions = import('@testspectra/matchers').SpectraStatic & TestSpectraCustomActions;\n`;
427
+ content += `declare const Spectra: TestSpectraActions;\n\n`;
428
+ // 3. Built-in Steps Declaration
429
+ content += `interface TestSpectraSteps {\n`;
430
+ const declaredSteps = new Set();
431
+ for (const stepsDir of stepsDirs) {
432
+ if (!fs.existsSync(stepsDir))
433
+ continue;
434
+ const stepEntries = fs.readdirSync(stepsDir, { withFileTypes: true });
435
+ for (const entry of stepEntries) {
436
+ if (entry.name.startsWith('.'))
437
+ continue;
438
+ if (entry.isDirectory()) {
439
+ const stepName = entry.name;
440
+ if (declaredSteps.has(stepName))
441
+ continue;
442
+ const stepFolder = path.join(stepsDir, stepName);
443
+ let matchedFile = null;
444
+ for (const stem of hierarchy) {
445
+ const cand1 = path.join(stepFolder, `${stem}.step.ts`);
446
+ const cand2 = path.join(stepFolder, `${stem}.ts`);
447
+ if (fs.existsSync(cand1)) {
448
+ matchedFile = path.relative(outputTypesDir, cand1).replace(/\\/g, '/').replace(/\.ts$/, '.js');
449
+ break;
450
+ }
451
+ else if (fs.existsSync(cand2)) {
452
+ matchedFile = path.relative(outputTypesDir, cand2).replace(/\\/g, '/').replace(/\.ts$/, '.js');
453
+ break;
454
+ }
455
+ }
456
+ if (matchedFile) {
457
+ if (!matchedFile.startsWith('.'))
458
+ matchedFile = `./${matchedFile}`;
459
+ const camel = stepName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
460
+ content += ` "${stepName}": (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
461
+ if (camel !== stepName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
462
+ content += ` ${camel}: (typeof import('${matchedFile}') extends { default: infer T } ? T : typeof import('${matchedFile}'));\n`;
463
+ }
464
+ declaredSteps.add(stepName);
465
+ }
466
+ }
467
+ else if (entry.isFile() && entry.name.endsWith('.ts')) {
468
+ const parts = entry.name.split('.');
469
+ const stepName = parts[0];
470
+ let stem = 'common';
471
+ if (parts.length > 2) {
472
+ stem = parts[1];
473
+ }
474
+ else if (parts.length === 2) {
475
+ stem = 'common';
476
+ }
477
+ if (hierarchy.includes(stem)) {
478
+ if (declaredSteps.has(stepName))
479
+ continue;
480
+ let rel = path
481
+ .relative(outputTypesDir, path.join(stepsDir, entry.name))
482
+ .replace(/\\/g, '/')
483
+ .replace(/\.ts$/, '.js');
484
+ if (!rel.startsWith('.'))
485
+ rel = `./${rel}`;
486
+ const camel = stepName.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
487
+ content += ` "${stepName}": (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
488
+ if (camel !== stepName && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(camel)) {
489
+ content += ` ${camel}: (typeof import('${rel}') extends { default: infer T } ? T : typeof import('${rel}'));\n`;
490
+ }
491
+ declaredSteps.add(stepName);
492
+ }
493
+ }
494
+ }
495
+ }
496
+ content += `}\n`;
497
+ content += `declare const Step: TestSpectraSteps;\n\n`;
498
+ results[platform] = content;
499
+ }
500
+ return results;
501
+ }
502
+ static generateFixturesDeclarationForScope(cwd, outputTypesDir, scopeDir, sharedDir) {
503
+ const fixturesDirs = this.getEntityDirsForScope('fixtures', scopeDir, sharedDir);
504
+ let content = `// Auto-generated ambient fixture declarations for TestSpectra\n`;
505
+ content += `// Managed automatically by TestSpectra Language Service Plugin.\n\n`;
506
+ content += `interface TestSpectraFixtures {\n`;
507
+ const declaredFixtures = new Set();
508
+ function scanFixtureDir(dir) {
509
+ if (!fs.existsSync(dir))
510
+ return;
511
+ try {
512
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
513
+ for (const entry of entries) {
514
+ if (entry.name.startsWith('.'))
515
+ continue;
516
+ if (entry.isDirectory()) {
517
+ scanFixtureDir(path.join(dir, entry.name));
518
+ }
519
+ else if (entry.isFile()) {
520
+ const parsed = path.parse(entry.name);
521
+ const varName = parsed.name.replace(/[^a-zA-Z0-9_$]/g, '_');
522
+ if (declaredFixtures.has(varName))
523
+ continue;
524
+ if (entry.name.endsWith('.json')) {
525
+ let rel = path.relative(outputTypesDir, path.join(dir, entry.name)).replace(/\\/g, '/');
526
+ if (!rel.startsWith('.'))
527
+ rel = `./${rel}`;
528
+ content += ` readonly ${varName}: typeof import('${rel}');\n`;
529
+ }
530
+ else {
531
+ content += ` readonly ${varName}: string;\n`;
532
+ }
533
+ declaredFixtures.add(varName);
534
+ }
535
+ }
536
+ }
537
+ catch { }
538
+ }
539
+ for (const fixturesDir of fixturesDirs) {
540
+ scanFixtureDir(fixturesDir);
541
+ }
542
+ content += `}\n`;
543
+ content += `declare const Fixture: TestSpectraFixtures;\n`;
544
+ return content;
545
+ }
546
+ static writeDeclarationFiles(cwd) {
547
+ const rootTypesDir = path.join(cwd, '.testspectra', 'types');
548
+ if (!fs.existsSync(rootTypesDir)) {
549
+ fs.mkdirSync(rootTypesDir, { recursive: true });
550
+ }
551
+ const scopes = this.getEntityScopes(cwd);
552
+ const sharedScope = scopes.find((s) => s.isShared);
553
+ const featureScopes = scopes.filter((s) => !s.isShared);
554
+ const hasScopes = Boolean(sharedScope || featureScopes.length > 0);
555
+ const hasRootEntities = fs.existsSync(path.join(cwd, 'specs')) ||
556
+ fs.existsSync(path.join(cwd, 'page-objects')) ||
557
+ fs.existsSync(path.join(cwd, 'pageobjects')) ||
558
+ fs.existsSync(path.join(cwd, 'support')) ||
559
+ fs.existsSync(path.join(cwd, 'fixtures'));
560
+ // 1. Generate Global / Standalone Root Types ONLY when there are root entities or no sub-scopes
561
+ if (!hasScopes || hasRootEntities) {
562
+ const rootFixtures = this.generateFixturesDeclarationForScope(cwd, rootTypesDir, cwd);
563
+ fs.writeFileSync(path.join(rootTypesDir, 'fixtures.d.ts'), rootFixtures, 'utf-8');
564
+ const rootDeclarations = this.generateDeclarationsForScope(cwd, rootTypesDir, cwd);
565
+ for (const [platform, content] of Object.entries(rootDeclarations)) {
566
+ fs.writeFileSync(path.join(rootTypesDir, `${platform}.d.ts`), content, 'utf-8');
567
+ }
568
+ }
569
+ else {
570
+ // In a pure monorepo where entities are partitioned in shared/ and packages/,
571
+ // purge any root platform .d.ts files so types are not emitted outside shared & package folders
572
+ const rootPlatformFiles = ['web.d.ts', 'mobile.d.ts', 'android.d.ts', 'ios.d.ts', 'common.d.ts', 'fixtures.d.ts'];
573
+ for (const f of rootPlatformFiles) {
574
+ const p = path.join(rootTypesDir, f);
575
+ if (fs.existsSync(p)) {
576
+ try {
577
+ fs.unlinkSync(p);
578
+ }
579
+ catch { }
580
+ }
581
+ }
582
+ }
583
+ // 2. Generate Scoped Types for Shared Library: .testspectra/types/shared/
584
+ if (sharedScope) {
585
+ const sharedTypesDir = path.join(rootTypesDir, 'shared');
586
+ if (!fs.existsSync(sharedTypesDir))
587
+ fs.mkdirSync(sharedTypesDir, { recursive: true });
588
+ const sharedFixtures = this.generateFixturesDeclarationForScope(cwd, sharedTypesDir, sharedScope.dir);
589
+ fs.writeFileSync(path.join(sharedTypesDir, 'fixtures.d.ts'), sharedFixtures, 'utf-8');
590
+ const sharedDeclarations = this.generateDeclarationsForScope(cwd, sharedTypesDir, sharedScope.dir);
591
+ for (const [platform, content] of Object.entries(sharedDeclarations)) {
592
+ fs.writeFileSync(path.join(sharedTypesDir, `${platform}.d.ts`), content, 'utf-8');
593
+ }
594
+ }
595
+ else {
596
+ const sharedTypesDir = path.join(rootTypesDir, 'shared');
597
+ if (fs.existsSync(sharedTypesDir)) {
598
+ try {
599
+ fs.rmSync(sharedTypesDir, { recursive: true, force: true });
600
+ }
601
+ catch { }
602
+ }
603
+ }
604
+ // 3. Generate Scoped Types for Feature Modules: .testspectra/types/features/<featureName>/
605
+ const activeFeatureNames = new Set(featureScopes.map((f) => f.name));
606
+ for (const feature of featureScopes) {
607
+ const featureTypesDir = path.join(rootTypesDir, 'features', feature.name);
608
+ if (!fs.existsSync(featureTypesDir))
609
+ fs.mkdirSync(featureTypesDir, { recursive: true });
610
+ const featureFixtures = this.generateFixturesDeclarationForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
611
+ fs.writeFileSync(path.join(featureTypesDir, 'fixtures.d.ts'), featureFixtures, 'utf-8');
612
+ const featureDeclarations = this.generateDeclarationsForScope(cwd, featureTypesDir, feature.dir, sharedScope?.dir);
613
+ for (const [platform, content] of Object.entries(featureDeclarations)) {
614
+ fs.writeFileSync(path.join(featureTypesDir, `${platform}.d.ts`), content, 'utf-8');
615
+ }
616
+ }
617
+ // Clean up stale feature folders
618
+ const featuresBaseDir = path.join(rootTypesDir, 'features');
619
+ if (fs.existsSync(featuresBaseDir)) {
620
+ try {
621
+ const existingFeatureDirs = fs.readdirSync(featuresBaseDir, { withFileTypes: true });
622
+ for (const entry of existingFeatureDirs) {
623
+ if (entry.isDirectory() && !activeFeatureNames.has(entry.name)) {
624
+ fs.rmSync(path.join(featuresBaseDir, entry.name), { recursive: true, force: true });
625
+ }
626
+ }
627
+ if (activeFeatureNames.size === 0) {
628
+ fs.rmSync(featuresBaseDir, { recursive: true, force: true });
629
+ }
630
+ }
631
+ catch { }
632
+ }
633
+ // Universal ambient index
634
+ const universalPath = path.join(rootTypesDir, 'index.d.ts');
635
+ if (sharedScope) {
636
+ fs.writeFileSync(universalPath, `/// <reference path="./shared/common.d.ts" />\n`, 'utf-8');
637
+ }
638
+ else {
639
+ fs.writeFileSync(universalPath, `/// <reference path="./common.d.ts" />\n`, 'utf-8');
640
+ }
641
+ // 4. Generate Scoped tsconfig solutions in .testspectra/tsconfig/
642
+ TsConfigGenerator.writeConfigs(cwd, scopes);
643
+ }
644
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,11 @@
1
- import { Command } from "commander";
2
- export * from "./config/schema.js";
3
- export * from "./config/loader.js";
4
- export * from "./types/generator.js";
5
- export * from "@testspectra/matchers";
1
+ import { Command } from 'commander';
2
+ export * from './config/schema.js';
3
+ export * from './config/loader.js';
4
+ export * from './generator/index.js';
5
+ export * from './lifecycle/index.js';
6
+ export * from './refactor/index.js';
7
+ export * from './mobile/index.js';
8
+ export * from './types/generator.js';
9
+ export * from '@testspectra/matchers';
6
10
  export declare function createCliProgram(): Command;
7
11
  export declare function runCli(args?: string[]): Promise<void>;