@nutin/cli 1.3.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +106 -9
  2. package/README.md +17 -35
  3. package/bin/add-feature.mjs +9 -0
  4. package/bin/cli.mjs +8 -0
  5. package/bin/create-app.mjs +5 -4
  6. package/bin/update-app.mjs +9 -0
  7. package/lib/index.mjs +76 -25
  8. package/lib/src/add-feature/feature-adder.mjs +83 -0
  9. package/lib/src/add-feature/feature-context-builder.mjs +25 -0
  10. package/lib/src/add-feature/nutin-config-updater.mjs +29 -0
  11. package/lib/src/add-feature/package-json-updater.mjs +49 -0
  12. package/lib/src/common/feature-registry.mjs +11 -0
  13. package/lib/src/common/file-generator.mjs +159 -0
  14. package/lib/src/common/meta-bootstrap-prompt.mjs +33 -0
  15. package/lib/src/common/package-data.mjs +10 -0
  16. package/lib/src/common/package-json-helper.mjs +103 -0
  17. package/lib/src/common/project-meta.mjs +54 -0
  18. package/lib/src/create-app/context-builder.mjs +21 -0
  19. package/lib/src/{git-manager.mjs → create-app/git-manager.mjs} +2 -2
  20. package/lib/src/create-app/json-manager.mjs +73 -0
  21. package/lib/src/{prompts.mjs → create-app/new-app-prompt.mjs} +6 -6
  22. package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +10 -8
  23. package/lib/src/create-app/success-message.mjs +13 -0
  24. package/lib/src/update-app/conflict-report-writer.mjs +31 -0
  25. package/lib/src/update-app/fetch-old-templates.mjs +61 -0
  26. package/lib/src/update-app/project-updater.mjs +142 -0
  27. package/lib/src/update-app/template-differ.mjs +124 -0
  28. package/lib/src/update-app/update-context-builder.mjs +25 -0
  29. package/lib/src/update-app/update-summary-printer.mjs +22 -0
  30. package/lib/src/update-app/version-compare.mjs +12 -0
  31. package/lib/src/utils/print.mjs +36 -0
  32. package/lib/src/utils/promise-exec-alias.mjs +4 -0
  33. package/package.json +17 -12
  34. package/templates/base/.gitignore.hbs +6 -1
  35. package/templates/base/AGENTS.md.hbs +168 -0
  36. package/templates/base/GETTING_STARTED.md.hbs +199 -0
  37. package/templates/base/README.md.hbs +8 -17
  38. package/templates/base/config/languages.json.hbs +6 -0
  39. package/templates/base/config/seo.json.hbs +12 -0
  40. package/templates/base/nutin.config.js.hbs +44 -0
  41. package/templates/base/src/app/components/index.ts.hbs +1 -2
  42. package/templates/base/src/app/globals.d.ts.hbs +3 -19
  43. package/templates/base/src/{libs/guards → app}/guards.ts.hbs +1 -5
  44. package/templates/base/src/app/main.ts.hbs +6 -26
  45. package/templates/base/src/app/services/index.ts.hbs +1 -1
  46. package/templates/base/src/app/views/home/home.view.html.hbs +7 -0
  47. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -57
  48. package/templates/base/src/app/views/index.ts.hbs +1 -0
  49. package/templates/base/src/app/views/not-found/not-found.view.html.hbs +8 -0
  50. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +5 -38
  51. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +158 -0
  52. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +68 -35
  53. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +124 -0
  54. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +9 -8
  55. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +96 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +6 -3
  57. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +151 -0
  58. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +8 -2
  59. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +88 -0
  60. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +2 -2
  61. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +62 -0
  62. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +89 -0
  63. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +4 -1
  64. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +142 -0
  65. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +67 -22
  66. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +183 -0
  67. package/templates/base/src/core/base-classes/component/component.test.js.hbs +85 -0
  68. package/templates/base/src/core/base-classes/component/component.ts.hbs +24 -33
  69. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +58 -0
  70. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +75 -0
  71. package/templates/base/src/core/base-classes/service/service.test.js.hbs +150 -0
  72. package/templates/base/src/core/base-classes/service/service.ts.hbs +26 -20
  73. package/templates/base/src/core/base-classes/view/view.test.js.hbs +71 -0
  74. package/templates/base/src/core/base-classes/view/view.ts.hbs +24 -49
  75. package/templates/base/src/core/config.ts.hbs +10 -0
  76. package/templates/base/src/core/index.ts.hbs +0 -3
  77. package/templates/base/src/core/internals.d.ts.hbs +27 -0
  78. package/templates/base/src/core/nutin-config.d.ts.hbs +4 -0
  79. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +67 -3
  80. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +9 -4
  81. package/templates/base/src/core/services/event-bus/index.ts.hbs +3 -0
  82. package/templates/base/src/core/services/event-bus/lifecycle.facade.test.js.hbs +123 -0
  83. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +61 -0
  84. package/templates/base/src/core/services/event-bus/navigation.facade.test.js.hbs +46 -0
  85. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +25 -0
  86. package/templates/base/src/core/services/globals/globals.test.js.hbs +187 -0
  87. package/templates/base/src/core/services/globals/globals.ts.hbs +104 -0
  88. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +25 -0
  89. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +20 -0
  90. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +73 -2
  91. package/templates/base/src/core/services/http-client/http-client.ts.hbs +4 -1
  92. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  93. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +22 -4
  94. package/templates/base/src/core/services/index.ts.hbs +5 -6
  95. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  96. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  97. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  98. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +257 -6
  100. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +56 -29
  101. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  102. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  103. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +14 -38
  104. package/templates/base/src/core/services/router/router.test.js.hbs +304 -2
  105. package/templates/base/src/core/services/router/router.ts.hbs +47 -12
  106. package/templates/base/src/index.html.hbs +2 -6
  107. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  108. package/templates/base/src/styles/base/_variables.scss.hbs +1 -86
  109. package/templates/base/src/styles/main.scss.hbs +1 -18
  110. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  111. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  112. package/templates/base/tools/builder/builder.js.hbs +24 -26
  113. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  114. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  115. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  116. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  117. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  118. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  119. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  120. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  121. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  122. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  123. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  124. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  125. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  126. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  127. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  128. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  129. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  130. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  131. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  132. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  133. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  134. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  136. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  137. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  138. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  139. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  140. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  141. package/templates/base/tools/dev/serve.js.hbs +11 -1
  142. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  143. package/templates/base/tools/generator/generator.js.hbs +44 -47
  144. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  145. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  146. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  147. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  148. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  149. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  150. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  151. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +8 -0
  152. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +17 -0
  153. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  154. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  155. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  156. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  157. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  158. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  159. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +4 -3
  160. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  164. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  165. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  166. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  167. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  168. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  169. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  170. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  171. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  172. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  173. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  174. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  175. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  176. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  177. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  178. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  179. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  180. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  181. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  182. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  183. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  185. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  186. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  187. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  188. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  189. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  190. package/templates/base/tools/utils/format.js.hbs +10 -10
  191. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  192. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  193. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  194. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  195. package/templates/base/tools/utils/index.js.hbs +1 -3
  196. package/templates/base/tools/utils/languages.js.hbs +15 -0
  197. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  198. package/templates/base/tools/utils/print.js.hbs +19 -13
  199. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  200. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  201. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  202. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  203. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  204. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  205. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  206. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  207. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  208. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  209. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  211. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  212. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  213. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  214. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  215. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  216. package/lib/src/context-builder.mjs +0 -40
  217. package/lib/src/file-generator.mjs +0 -109
  218. package/lib/src/json-generator.mjs +0 -31
  219. package/lib/src/package-manager.mjs +0 -139
  220. package/lib/src/print.mjs +0 -32
  221. package/lib/src/utils.mjs +0 -18
  222. package/lib/src/version.mjs +0 -1
  223. package/templates/base/builder.config.js.hbs +0 -36
  224. package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +0 -32
  225. package/templates/base/src/app/components/common/button/button.component.ts.hbs +0 -40
  226. package/templates/base/src/app/dom.test.js.hbs +0 -13
  227. package/templates/base/src/app/services/example/example.service.ts.hbs +0 -18
  228. package/templates/base/src/app/views/home/home.view.test.js.hbs +0 -26
  229. package/templates/base/src/core/services/store/store.test.js.hbs +0 -8
  230. package/templates/base/src/core/services/store/store.ts.hbs +0 -39
  231. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.test.js.hbs +0 -0
  232. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +0 -113
  233. package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +0 -249
  234. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +0 -131
  235. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +0 -50
  236. package/templates/base/src/core/utils/index.ts.hbs +0 -2
  237. package/templates/base/src/libs/index.ts.hbs +0 -4
  238. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +0 -197
  239. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +0 -75
  240. package/templates/base/src/libs/popover/popover.ts.hbs +0 -138
  241. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -75
  242. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  243. package/templates/base/src/styles/core/_anchors.scss.hbs +0 -10
  244. package/templates/base/src/styles/core/_index.scss.hbs +0 -6
  245. package/templates/base/src/styles/core/_popover.scss.hbs +0 -96
  246. package/templates/base/src/styles/core/_snackbar.scss.hbs +0 -50
  247. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  248. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  249. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  250. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  251. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  252. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  253. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  254. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  255. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  256. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  257. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  258. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  259. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  260. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  261. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  262. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  263. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  264. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  265. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -254
  266. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  267. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  268. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  269. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  270. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  271. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  272. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  273. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  274. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  275. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  276. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  277. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  278. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  279. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  280. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  281. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  282. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  283. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  284. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  285. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  286. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  287. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  288. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  289. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -1,12 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { print{{#if i18n}}, LANGUAGES{{/if}} } from '../utils/index.js';
4
- {{#if i18n}}
5
- import { jsonTemplate } from './templates/index.js';
6
- {{/if}}
7
- {{#if stylinNutin}}
8
- import { scssTemplate } from './templates/index.js';
9
- {{/if}}
3
+ import { print, LANGUAGES } from '../utils/index.js';
4
+ import { localeTemplate } from './templates/index.js';
10
5
 
11
6
  export function generateFile({
12
7
  name,
@@ -16,7 +11,7 @@ export function generateFile({
16
11
  extension = 'ts',
17
12
  }) {
18
13
  fs.mkdirSync(targetPath, { recursive: true });
19
- const template = templateFn(name, targetPath);
14
+ const template = templateFn(name, targetPath, suffix);
20
15
  const filePath = `${targetPath}/${name.kebab}.${suffix}.${extension}`;
21
16
 
22
17
  if (fs.existsSync(filePath)) {
@@ -42,51 +37,29 @@ export function appendToIndex({ name, targetPath, suffix }) {
42
37
 
43
38
  try {
44
39
  fs.appendFileSync(indexFilePath, lineToAppend, 'utf8');
45
- print.info(`${suffix}s/index.ts updated.`);
40
+ print.gray(`${suffix}s/index.ts updated.`);
46
41
  } catch (err) {
47
- print.error(`Error appending line: ${err}`);
42
+ throw new Error(`Failed to update ${indexFilePath}: ${err.message}`, { cause: err });
48
43
  }
49
44
  }
50
45
 
51
- {{#if i18n}}
52
- export function generateJson({ targetPath, name }) {
46
+ export function generateLocalesJson({ targetPath, name, isView }) {
53
47
  const localesDir = `${targetPath}/locales`;
54
48
  fs.mkdirSync(localesDir, { recursive: true });
55
49
 
56
- const template = targetPath.includes('view') ? jsonTemplate(name) : `{ "default": "${name.pascal} works !"}`;
50
+ const template = localeTemplate(name, isView);
57
51
 
58
- // include "meta" keys for views, empty JSON for components
59
- LANGUAGES.forEach((lang) => {
60
- fs.writeFileSync(`${localesDir}/${lang}.json`, template);
61
- });
62
- }
63
-
64
- {{/if}}
65
- {{#if stylinNutin}}
66
- export function generateStylesheet({ name, suffix }) {
67
- const template = scssTemplate();
68
- const stylesPath = path.join('src', 'styles', `${suffix}s`);
69
- const filePath = path.join(stylesPath, `_${name.kebab}.${suffix}.scss`);
70
- const indexPath = path.join(stylesPath, `_index.scss`);
71
-
72
- if (!fs.existsSync(stylesPath) || !fs.existsSync(indexPath)) {
73
- print.boldError('Non existing path');
74
- process.exit(1);
75
- }
76
- if (fs.existsSync(filePath)) {
77
- print.boldError('A file with this name already exists');
78
- process.exit(1);
52
+ const failedLangs = [];
53
+ for (const lang of LANGUAGES) {
54
+ try {
55
+ fs.writeFileSync(`${localesDir}/${lang}.json`, template);
56
+ } catch (err) {
57
+ print.error(`Failed to write locale file for "${lang}": ${err.message}`);
58
+ failedLangs.push(lang);
59
+ }
79
60
  }
80
61
 
81
- fs.writeFileSync(filePath, template);
82
-
83
- const lineToAppend = `@forward "${name.kebab}.${suffix}";\n`;
84
-
85
- try {
86
- fs.appendFileSync(indexPath, lineToAppend, 'utf8');
87
- } catch (err) {
88
- print.error(`Error appending line: ${err}`);
62
+ if (failedLangs.length) {
63
+ throw new Error(`Failed to generate locale file(s) for: ${failedLangs.join(', ')}`);
89
64
  }
90
65
  }
91
-
92
- {{/if}}
@@ -0,0 +1,72 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { generateFile, appendToIndex } from './handle-file.js';
5
+
6
+ describe('handle-file', () => {
7
+ let tmpDir;
8
+ let originalCwd;
9
+ let exitSpy;
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-handle-file-'));
13
+ originalCwd = process.cwd();
14
+ exitSpy = spyOn(process, 'exit').andCallFake(() => {});
15
+ });
16
+
17
+ afterEach(() => {
18
+ exitSpy.restore();
19
+ process.chdir(originalCwd);
20
+ fs.rmSync(tmpDir, { recursive: true, force: true });
21
+ });
22
+
23
+ it('generateFile writes the template output to <targetPath>/<name>.<suffix>.<extension>', () => {
24
+ const targetPath = path.join(tmpDir, 'components', 'widget');
25
+ const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
26
+
27
+ generateFile({ name, targetPath, templateFn: (n) => `content for ${n.kebab}`, suffix: 'component' });
28
+
29
+ const filePath = path.join(targetPath, 'widget.component.ts');
30
+ expect(fs.existsSync(filePath)).toBeTruthy();
31
+ expect(fs.readFileSync(filePath, 'utf8')).toBe('content for widget');
32
+ expect(exitSpy.callCount).toBe(0);
33
+ });
34
+
35
+ it('generateFile respects a custom extension', () => {
36
+ const targetPath = path.join(tmpDir, 'services', 'widget');
37
+ const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
38
+
39
+ generateFile({ name, targetPath, templateFn: () => 'x', suffix: 'service', extension: 'js' });
40
+
41
+ expect(fs.existsSync(path.join(targetPath, 'widget.service.js'))).toBeTruthy();
42
+ });
43
+
44
+ it('generateFile calls process.exit(1) instead of overwriting an existing file', () => {
45
+ const targetPath = path.join(tmpDir, 'components', 'widget');
46
+ const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
47
+ const templateFn = () => 'fixed content';
48
+
49
+ generateFile({ name, targetPath, templateFn, suffix: 'component' });
50
+ // the second call hits the already-exists guard, which prints via console.error
51
+ silenceConsole('error', () => generateFile({ name, targetPath, templateFn, suffix: 'component' }));
52
+
53
+ expect(exitSpy.callCount).toBe(1);
54
+ expect(exitSpy.lastCall).toEqual([1]);
55
+ });
56
+
57
+ it('appendToIndex appends an export line to the barrel three segments up from targetPath', () => {
58
+ process.chdir(tmpDir);
59
+ const basePath = path.join('src', 'app', 'components');
60
+ fs.mkdirSync(basePath, { recursive: true });
61
+ fs.writeFileSync(path.join(basePath, 'index.ts'), "export * from './existing.js';\n");
62
+
63
+ const targetPath = path.join('src', 'app', 'components', 'widget');
64
+ const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
65
+
66
+ // appendToIndex prints a "index.ts updated" log via console.info
67
+ silenceConsole('info', () => appendToIndex({ name, targetPath, suffix: 'component' }));
68
+
69
+ const indexContent = fs.readFileSync(path.join(basePath, 'index.ts'), 'utf8');
70
+ expect(indexContent).toBe("export * from './existing.js';\nexport * from './widget/widget.component.js';\n");
71
+ });
72
+ });
@@ -1,11 +1,12 @@
1
- import { getRelToCore } from "../../utils/get-rel-to-core.js";
1
+ import { getRelToCore } from "../../../utils/get-rel-to-core.js";
2
2
 
3
3
  export const componentTemplate = (name, targetPath) => {
4
4
  const relToCore = getRelToCore(targetPath);
5
+ const htmlTemplate = '__TEMPLATE_PLACEHOLDER__';
5
6
 
6
7
  return `import { Component } from '${relToCore}';
7
8
 
8
- const templateFn = () => {{#if template}}\`__TEMPLATE_PLACEHOLDER__\`{{else}}\`<div>${name.pascal} works !</div>\`{{/if}};
9
+ const templateFn = () => \`${htmlTemplate}\`;
9
10
 
10
11
  export class ${name.pascal}Component extends Component {
11
12
  constructor(mountTarget: HTMLElement) {
@@ -0,0 +1,23 @@
1
+ import { componentTemplate } from './component.template.js';
2
+
3
+ describe('componentTemplate', () => {
4
+ it('renders a component class importing Component via the relative core path', () => {
5
+ const name = { pascal: 'Widget' };
6
+ const targetPath = 'src/app/components/widget';
7
+
8
+ // The generator always leaves the html body as a placeholder — the builder
9
+ // swaps in the .html sibling's content at build time.
10
+ expect(componentTemplate(name, targetPath)).toBe(
11
+ `import { Component } from '../../../core/index.js';
12
+
13
+ const templateFn = () => \`__TEMPLATE_PLACEHOLDER__\`;
14
+
15
+ export class WidgetComponent extends Component {
16
+ constructor(mountTarget: HTMLElement) {
17
+ super({templateFn, mountTarget});
18
+ }
19
+ }
20
+ `
21
+ );
22
+ });
23
+ });
@@ -0,0 +1,2 @@
1
+ export const htmlTemplate = (name) => `<div>${name.pascal} works !</div>
2
+ `;
@@ -0,0 +1,7 @@
1
+ import { htmlTemplate } from './html.template.js';
2
+
3
+ describe('htmlTemplate', () => {
4
+ it('renders a placeholder div with the component/view name', () => {
5
+ expect(htmlTemplate({ pascal: 'Widget' })).toBe('<div>Widget works !</div>\n');
6
+ });
7
+ });
@@ -1,12 +1,7 @@
1
- export * from './component.template.js';
2
- export * from './view.template.js';
3
- export * from './service.template.js';
4
- {{#if template}}
5
- export * from './html.template.js';
6
- {{/if}}
7
- {{#if i18n}}
8
- export * from './json.template.js';
9
- {{/if}}
10
- {{#if stylinNutin}}
11
- export * from './scss.template.js';
12
- {{/if}}
1
+ export * from './component/component.template.js';
2
+ export * from './view/view.template.js';
3
+ export * from './service/service.template.js';
4
+ export * from './locale/locale.template.js';
5
+ export * from './scss/scss.template.js';
6
+ export * from './html/html.template.js';
7
+ export * from './test/test.template.js';
@@ -0,0 +1,8 @@
1
+ export const localeTemplate = (name, isView) => isView ? `{
2
+ "title": "${name.capitalized}",
3
+ "default": "${name.pascal} works !"
4
+ }
5
+ ` : `{
6
+ "default": "${name.pascal} works !"
7
+ }
8
+ `;
@@ -0,0 +1,17 @@
1
+ import { localeTemplate } from './locale.template.js';
2
+
3
+ describe('localeTemplate', () => {
4
+ it('renders a locale JSON stub with a "default" key', () => {
5
+ expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' })).toBe('{\n "default": "Widget works !"\n}\n');
6
+ });
7
+
8
+ it('does not include a "title" key when isView is false', () => {
9
+ expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' }, false)).toBe('{\n "default": "Widget works !"\n}\n');
10
+ });
11
+
12
+ it('includes a top-level "title" key seeded from the capitalized name when isView is true', () => {
13
+ expect(localeTemplate({ pascal: 'Widget', capitalized: 'Widget' }, true)).toBe(
14
+ '{\n "title": "Widget",\n "default": "Widget works !"\n}\n'
15
+ );
16
+ });
17
+ });
@@ -0,0 +1 @@
1
+ export const scssTemplate = () => `\n`;
@@ -0,0 +1,7 @@
1
+ import { scssTemplate } from './scss.template.js';
2
+
3
+ describe('scssTemplate', () => {
4
+ it('renders an empty stylesheet placeholder', () => {
5
+ expect(scssTemplate()).toBe('\n');
6
+ });
7
+ });
@@ -0,0 +1,17 @@
1
+ import { getRelToCore } from "../../../utils/get-rel-to-core.js";
2
+
3
+ export const serviceTemplate = (name, targetPath) => {
4
+ const relToCore = getRelToCore(targetPath);
5
+
6
+ return `import { Service } from '${relToCore}';
7
+
8
+ export class ${name.pascal}Service extends Service<${name.pascal}Service> {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ }
14
+
15
+ export const ${name.camel}Service = ${name.pascal}Service.getInstance();
16
+ `;
17
+ }
@@ -0,0 +1,22 @@
1
+ import { serviceTemplate } from './service.template.js';
2
+
3
+ describe('serviceTemplate', () => {
4
+ it('renders a Service subclass with a singleton export, importing Service via the relative core path', () => {
5
+ const name = { pascal: 'Widget', camel: 'widget' };
6
+ const targetPath = 'src/app/components/widget';
7
+
8
+ expect(serviceTemplate(name, targetPath)).toBe(
9
+ `import { Service } from '../../../core/index.js';
10
+
11
+ export class WidgetService extends Service<WidgetService> {
12
+ constructor() {
13
+ super();
14
+ }
15
+
16
+ }
17
+
18
+ export const widgetService = WidgetService.getInstance();
19
+ `
20
+ );
21
+ });
22
+ });
@@ -0,0 +1,10 @@
1
+ export const testTemplate = (name, targetPath, suffix) => {
2
+ const type = suffix.charAt(0).toUpperCase() + suffix.slice(1);
3
+
4
+ return `import { ${name.pascal}${type} } from '#root/dist/${targetPath}/${name.kebab}.${suffix}.js';
5
+
6
+ describe('${name.pascal}${type}', () => {
7
+ it.todo('Write tests for ${name.pascal}${type}');
8
+ });
9
+ `;
10
+ }
@@ -0,0 +1,27 @@
1
+ import { testTemplate } from './test.template.js';
2
+
3
+ describe('testTemplate', () => {
4
+ it('renders an it.todo stub importing the built output via #root/dist', () => {
5
+ const name = { pascal: 'Widget', kebab: 'widget' };
6
+ const targetPath = 'src/app/components/widget';
7
+
8
+ expect(testTemplate(name, targetPath, 'component')).toBe(
9
+ `import { WidgetComponent } from '#root/dist/src/app/components/widget/widget.component.js';
10
+
11
+ describe('WidgetComponent', () => {
12
+ it.todo('Write tests for WidgetComponent');
13
+ });
14
+ `
15
+ );
16
+ });
17
+
18
+ it('capitalizes only the first letter of the suffix for the "Type" suffix on the class name', () => {
19
+ const name = { pascal: 'Home', kebab: 'home' };
20
+ const targetPath = 'src/app/views/home';
21
+
22
+ const result = testTemplate(name, targetPath, 'view');
23
+
24
+ expect(result).toContain('HomeView');
25
+ expect(result).toContain("it.todo('Write tests for HomeView');");
26
+ });
27
+ });
@@ -1,15 +1,16 @@
1
- import { getRelToCore } from "../../utils/get-rel-to-core.js";
1
+ import { getRelToCore } from "../../../utils/get-rel-to-core.js";
2
2
 
3
3
  export const viewTemplate = (name, targetPath) => {
4
4
  const relToCore = getRelToCore(targetPath);
5
+ const htmlTemplate = '__TEMPLATE_PLACEHOLDER__';
5
6
 
6
7
  return `import { View } from '${relToCore}';
7
8
 
8
- const template = {{#if template}}\`__TEMPLATE_PLACEHOLDER__\`{{else}}\`<div>${name.pascal} works !</div>\`{{/if}};
9
+ const template = \`${htmlTemplate}\`;
9
10
 
10
11
  export class ${name.pascal}View extends View {
11
12
  constructor() {
12
- super({template});
13
+ super({ template, viewName: '${name.kebab}' });
13
14
  }
14
15
 
15
16
  }
@@ -0,0 +1,24 @@
1
+ import { viewTemplate } from './view.template.js';
2
+
3
+ describe('viewTemplate', () => {
4
+ it('renders a view class importing View via the relative core path', () => {
5
+ const name = { kebab: 'widget', pascal: 'Widget' };
6
+ const targetPath = 'src/app/components/widget';
7
+
8
+ // The generator always leaves the html body as a placeholder — the builder
9
+ // swaps in the .html sibling's content at build time.
10
+ expect(viewTemplate(name, targetPath)).toBe(
11
+ `import { View } from '../../../core/index.js';
12
+
13
+ const template = \`__TEMPLATE_PLACEHOLDER__\`;
14
+
15
+ export class WidgetView extends View {
16
+ constructor() {
17
+ super({ template, viewName: 'widget' });
18
+ }
19
+
20
+ }
21
+ `
22
+ );
23
+ });
24
+ });
@@ -0,0 +1,39 @@
1
+ import { Session } from 'node:inspector/promises';
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+ import { errorExit } from '../../../utils/index.js';
5
+
6
+ let session;
7
+
8
+ export async function startCoverage() {
9
+ try {
10
+ session = new Session();
11
+ session.connect();
12
+ await session.post('Profiler.enable');
13
+ await session.post('Profiler.startPreciseCoverage', { callCount: true, detailed: true });
14
+ } catch (err) {
15
+ errorExit(err, 'coverage/start');
16
+ }
17
+ }
18
+
19
+ export async function stopCoverage(scopedDirs) {
20
+ let result;
21
+ try {
22
+ ({ result } = await session.post('Profiler.takePreciseCoverage'));
23
+ await session.post('Profiler.stopPreciseCoverage');
24
+ await session.post('Profiler.disable');
25
+ session.disconnect();
26
+ } catch (err) {
27
+ errorExit(err, 'coverage/stop');
28
+ }
29
+
30
+ return result
31
+ .filter(entry => entry.url.startsWith('file://'))
32
+ .map(entry => ({ ...entry, filePath: fileURLToPath(entry.url) }))
33
+ .filter(({ filePath }) => {
34
+ const rel = path.relative(process.cwd(), filePath);
35
+ if (rel.includes(`node_modules${path.sep}`)) return false;
36
+ if (rel.endsWith('.test.js')) return false;
37
+ return scopedDirs.some(dir => rel.startsWith(dir + path.sep));
38
+ });
39
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { errorExit } from '../../../utils/index.js';
4
+
5
+ // V8 ranges within a script are always disjoint or strictly nested, never partially
6
+ // overlapping. The smallest range containing an offset holds the true execution count
7
+ // for that offset - e.g. "this function ran 5x but this one branch inside it ran 0x".
8
+ function countAt(offset, ranges) {
9
+ let best = null;
10
+ for (const range of ranges) {
11
+ if (offset < range.startOffset || offset >= range.endOffset) continue;
12
+ if (!best || (range.endOffset - range.startOffset) < (best.endOffset - best.startOffset)) {
13
+ best = range;
14
+ }
15
+ }
16
+ return best ? best.count : 0;
17
+ }
18
+
19
+ function buildLineStarts(sourceText) {
20
+ const starts = [0];
21
+ for (let i = 0; i < sourceText.length; i++) {
22
+ if (sourceText[i] === '\n') starts.push(i + 1);
23
+ }
24
+ return starts;
25
+ }
26
+
27
+ function offsetToLine(offset, lineStarts) {
28
+ let lo = 0, hi = lineStarts.length - 1;
29
+ while (lo < hi) {
30
+ const mid = (lo + hi + 1) >> 1;
31
+ if (lineStarts[mid] <= offset) lo = mid; else hi = mid - 1;
32
+ }
33
+ return lo + 1; // 1-indexed
34
+ }
35
+
36
+ // V8 sometimes reports an empty functionName (e.g. accessors, unnamed callbacks).
37
+ // Fall back to reading the source at the function's own start offset instead of a
38
+ // meaningless placeholder - covers `constructor(`, `get foo(`, method names, etc.
39
+ // Only treat a leading identifier as a real name when it's immediately followed by
40
+ // `(` (a param list) - otherwise it's just an arrow function's bare parameter (e.g.
41
+ // `c => ...`), which isn't a name at all, so fall through to a source snippet instead.
42
+ function deriveLabel(sourceText, startOffset) {
43
+ const snippet = sourceText.slice(startOffset, startOffset + 60);
44
+ const named = snippet.match(/^\s*(?:get |set )?([A-Za-z_$][\w$]*)\s*\(/);
45
+ if (named) return named[1];
46
+ return snippet.trim().split('\n')[0].slice(0, 30) || '<anonymous>';
47
+ }
48
+
49
+ export function computeFileCoverage({ functions }, sourceText) {
50
+ const allRanges = functions.flatMap(fn => fn.ranges);
51
+ const lineStarts = buildLineStarts(sourceText);
52
+
53
+ // Line coverage: sample the count at each line's first non-whitespace character.
54
+ // Not statement/branch-accurate, but simple and good enough for a lightweight report.
55
+ let offset = 0;
56
+ let totalLines = 0;
57
+ let coveredLines = 0;
58
+ const uncoveredLines = [];
59
+ for (const line of sourceText.split('\n')) {
60
+ const firstChar = line.search(/\S/);
61
+ if (firstChar !== -1) {
62
+ totalLines++;
63
+ if (countAt(offset + firstChar, allRanges) > 0) coveredLines++;
64
+ else uncoveredLines.push(offsetToLine(offset + firstChar, lineStarts));
65
+ }
66
+ offset += line.length + 1;
67
+ }
68
+
69
+ // Function coverage: every functions[] entry except the whole-script pseudo-function,
70
+ // identified by its first range spanning the entire file.
71
+ const realFunctions = functions.filter(fn => {
72
+ const [topRange] = fn.ranges;
73
+ return !(topRange.startOffset === 0 && topRange.endOffset === sourceText.length);
74
+ });
75
+ const coveredFunctions = realFunctions.filter(fn => fn.ranges[0].count > 0).length;
76
+ const uncoveredFunctions = realFunctions
77
+ .filter(fn => fn.ranges[0].count === 0)
78
+ .map(fn => ({
79
+ name: fn.functionName || deriveLabel(sourceText, fn.ranges[0].startOffset),
80
+ line: offsetToLine(fn.ranges[0].startOffset, lineStarts),
81
+ }));
82
+
83
+ // Branch coverage: every range beyond a function's own top-level range (index 0)
84
+ // is a nested conditional block V8 already tracked while collecting detailed
85
+ // coverage - if/else arms, ternary branches, switch cases, logical short-circuits,
86
+ // loop bodies. No AST parsing needed, just reusing data already being collected.
87
+ const allBranchRanges = functions.flatMap(fn => fn.ranges.slice(1));
88
+ const coveredBranches = allBranchRanges.filter(r => r.count > 0).length;
89
+ const uncoveredBranches = allBranchRanges
90
+ .filter(r => r.count === 0)
91
+ .map(r => offsetToLine(r.startOffset, lineStarts))
92
+ .sort((a, b) => a - b);
93
+
94
+ return {
95
+ totalLines, coveredLines, uncoveredLines,
96
+ linesPct: totalLines ? (coveredLines / totalLines) * 100 : 100,
97
+ totalBranches: allBranchRanges.length, coveredBranches, uncoveredBranches,
98
+ branchesPct: allBranchRanges.length ? (coveredBranches / allBranchRanges.length) * 100 : 100,
99
+ totalFunctions: realFunctions.length, coveredFunctions, uncoveredFunctions,
100
+ functionsPct: realFunctions.length ? (coveredFunctions / realFunctions.length) * 100 : 100,
101
+ };
102
+ }
103
+
104
+ export function buildCoverageReport(scopedResults) {
105
+ return scopedResults.map(entry => {
106
+ let sourceText;
107
+ try {
108
+ sourceText = fs.readFileSync(entry.filePath, 'utf-8');
109
+ } catch (err) {
110
+ errorExit(err, 'coverage/compute-coverage');
111
+ }
112
+ const coverage = computeFileCoverage(entry, sourceText);
113
+ return { file: path.relative(process.cwd(), entry.filePath), ...coverage };
114
+ });
115
+ }
@@ -0,0 +1,74 @@
1
+ import { computeFileCoverage } from './compute-coverage.js';
2
+
3
+ describe('computeFileCoverage', () => {
4
+ it('reports 100% when every function and line ran', () => {
5
+ const sourceText = 'function covered() {\n return 1;\n}\n';
6
+ const scriptResult = {
7
+ functions: [
8
+ { functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
9
+ { functionName: 'covered', ranges: [{ startOffset: 0, endOffset: sourceText.length - 1, count: 3 }] },
10
+ ],
11
+ };
12
+
13
+ const { linesPct, functionsPct, branchesPct } = computeFileCoverage(scriptResult, sourceText);
14
+ expect(linesPct).toEqual(100);
15
+ expect(functionsPct).toEqual(100);
16
+ expect(branchesPct).toEqual(100); // no nested ranges anywhere -> defaults to 100
17
+ });
18
+
19
+ it('reports 0% for a function that never ran', () => {
20
+ const sourceText = 'function uncovered() {\n return 1;\n}\n';
21
+ const scriptResult = {
22
+ functions: [
23
+ { functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
24
+ { functionName: 'uncovered', ranges: [{ startOffset: 0, endOffset: sourceText.length - 1, count: 0 }] },
25
+ ],
26
+ };
27
+
28
+ const { linesPct, functionsPct } = computeFileCoverage(scriptResult, sourceText);
29
+ expect(linesPct).toEqual(0);
30
+ expect(functionsPct).toEqual(0);
31
+ });
32
+
33
+ it('lets a nested 0-count branch override its enclosing covered range', () => {
34
+ const sourceText = 'function branchy(flag) {\n if (flag) {\n return 1;\n }\n return 2;\n}\n';
35
+ const branchStart = sourceText.indexOf('return 1');
36
+ const branchEnd = sourceText.indexOf(';', branchStart) + 1;
37
+
38
+ const scriptResult = {
39
+ functions: [
40
+ { functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
41
+ {
42
+ functionName: 'branchy',
43
+ ranges: [
44
+ { startOffset: 0, endOffset: sourceText.length - 1, count: 5 },
45
+ { startOffset: branchStart, endOffset: branchEnd, count: 0 },
46
+ ],
47
+ },
48
+ ],
49
+ };
50
+
51
+ const { linesPct, totalBranches, coveredBranches, branchesPct } = computeFileCoverage(scriptResult, sourceText);
52
+ // every line covered except the never-taken "return 1;" branch
53
+ expect(linesPct).toEqual(5 / 6 * 100);
54
+ expect(totalBranches).toEqual(1);
55
+ expect(coveredBranches).toEqual(0);
56
+ expect(branchesPct).toEqual(0);
57
+ });
58
+
59
+ it('derives a name from the source when V8 reports an empty functionName', () => {
60
+ const sourceText = 'class Foo {\n constructor(x) {\n this.x = x;\n }\n}\n';
61
+ const constructorStart = sourceText.indexOf('constructor');
62
+ const constructorEnd = sourceText.indexOf('}', constructorStart) + 1;
63
+
64
+ const scriptResult = {
65
+ functions: [
66
+ { functionName: '', ranges: [{ startOffset: 0, endOffset: sourceText.length, count: 1 }] },
67
+ { functionName: '', ranges: [{ startOffset: constructorStart, endOffset: constructorEnd, count: 0 }] },
68
+ ],
69
+ };
70
+
71
+ const { uncoveredFunctions } = computeFileCoverage(scriptResult, sourceText);
72
+ expect(uncoveredFunctions).toEqual([{ name: 'constructor', line: 2 }]);
73
+ });
74
+ });