@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,4 +1,6 @@
1
1
  import { NavigationManager } from '#root/dist/src/core/services/router/helpers/navigation-manager.helper.js';
2
+ import { I18nService } from '#root/dist/src/core/services/index.js';
3
+ import { CONFIG } from '#root/dist/src/core/config.js';
2
4
 
3
5
  describe('NavigationManager', () => {
4
6
  it('should normalize paths by removing trailing slashes', () => {
@@ -8,12 +10,6 @@ describe('NavigationManager', () => {
8
10
  expect(NavigationManager.normalizePath('')).toBe('/');
9
11
  });
10
12
 
11
- it('should determine if pushState should be used', () => {
12
- expect(NavigationManager.shouldPushState(true, '/home', '/about')).toBe(true);
13
- expect(NavigationManager.shouldPushState(false, '/home', '/about')).toBe(false);
14
- expect(NavigationManager.shouldPushState(true, '/about', '/about')).toBe(false);
15
- });
16
-
17
13
  it('should call pushState when conditions are met', () => {
18
14
  const originalPushState = window.history.pushState;
19
15
  let called = false;
@@ -48,4 +44,259 @@ describe('NavigationManager', () => {
48
44
  const currentPath = window.location.pathname;
49
45
  expect(NavigationManager.getCurrentPath()).toBe(currentPath);
50
46
  });
47
+
48
+ it('extractLocale returns the original path untouched when CONFIG.i18n is disabled', () => {
49
+ CONFIG.i18n = false;
50
+ expect(NavigationManager.extractLocale('/fr/about')).toEqual({ locale: null, strippedPath: '/fr/about' });
51
+ });
52
+
53
+ it('extractLocale strips a recognized locale prefix when CONFIG.i18n is enabled', () => {
54
+ CONFIG.i18n = true;
55
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
56
+ try {
57
+ expect(NavigationManager.extractLocale('/fr/about')).toEqual({ locale: 'fr', strippedPath: '/about' });
58
+ expect(NavigationManager.extractLocale('/fr')).toEqual({ locale: 'fr', strippedPath: '/' });
59
+ } finally {
60
+ CONFIG.i18n = false;
61
+ }
62
+ });
63
+
64
+ it('extractLocale leaves the path untouched when the first segment is not a recognized locale', () => {
65
+ CONFIG.i18n = true;
66
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
67
+ try {
68
+ expect(NavigationManager.extractLocale('/about')).toEqual({ locale: null, strippedPath: '/about' });
69
+ } finally {
70
+ CONFIG.i18n = false;
71
+ }
72
+ });
73
+
74
+ it('addLocalePrefix returns the path unchanged when CONFIG.i18n is disabled', () => {
75
+ CONFIG.i18n = false;
76
+ expect(NavigationManager.addLocalePrefix('/about')).toBe('/about');
77
+ });
78
+
79
+ it('addLocalePrefix prefixes the path with the current language when CONFIG.i18n is enabled', () => {
80
+ CONFIG.i18n = true;
81
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
82
+ I18nService['_currentLanguage'] = 'fr';
83
+ try {
84
+ expect(NavigationManager.addLocalePrefix('/about')).toBe('/fr/about');
85
+ expect(NavigationManager.addLocalePrefix('/')).toBe('/fr');
86
+ } finally {
87
+ CONFIG.i18n = false;
88
+ I18nService['_currentLanguage'] = 'en';
89
+ }
90
+ });
91
+
92
+ it('getCurrentLocale reads the locale from the current URL path when CONFIG.i18n is enabled', () => {
93
+ CONFIG.i18n = true;
94
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
95
+ window.history.pushState({}, '', '/fr/about');
96
+ try {
97
+ expect(NavigationManager.getCurrentLocale()).toBe('fr');
98
+ } finally {
99
+ CONFIG.i18n = false;
100
+ window.history.pushState({}, '', '/');
101
+ }
102
+ });
103
+
104
+ it('getCurrentLocale returns null when CONFIG.i18n is disabled', () => {
105
+ window.history.pushState({}, '', '/fr/about');
106
+ try {
107
+ expect(NavigationManager.getCurrentLocale()).toBe(null);
108
+ } finally {
109
+ window.history.pushState({}, '', '/');
110
+ }
111
+ });
112
+
113
+ it('updateLocaleInUrl pushes a new URL when the locale-prefixed path differs from the current one', () => {
114
+ CONFIG.i18n = true;
115
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
116
+ I18nService['_currentLanguage'] = 'fr';
117
+ window.history.pushState({}, '', '/about');
118
+
119
+ const originalPushState = window.history.pushState;
120
+ const calls = [];
121
+ window.history.pushState = (...args) => {
122
+ calls.push(args);
123
+ originalPushState.apply(window.history, args);
124
+ };
125
+
126
+ try {
127
+ NavigationManager.updateLocaleInUrl();
128
+ expect(calls.length).toBe(1);
129
+ expect(calls[0][2]).toBe('/fr/about');
130
+ } finally {
131
+ window.history.pushState = originalPushState;
132
+ CONFIG.i18n = false;
133
+ I18nService['_currentLanguage'] = 'en';
134
+ window.history.pushState({}, '', '/');
135
+ }
136
+ });
137
+
138
+ it('updateLocaleInUrl handles a root pathname that collapses to an empty string when stripped', () => {
139
+ CONFIG.i18n = false;
140
+ window.history.pushState({}, '', '/');
141
+
142
+ const originalPushState = window.history.pushState;
143
+ window.history.pushState = () => {};
144
+
145
+ try {
146
+ expect(() => NavigationManager.updateLocaleInUrl()).not.toThrow();
147
+ } finally {
148
+ window.history.pushState = originalPushState;
149
+ }
150
+ });
151
+
152
+ it('updateLocaleInUrl does nothing when the URL already has the correct locale prefix', () => {
153
+ CONFIG.i18n = true;
154
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
155
+ I18nService['_currentLanguage'] = 'fr';
156
+ window.history.pushState({}, '', '/fr/about');
157
+
158
+ const originalPushState = window.history.pushState;
159
+ let called = false;
160
+ window.history.pushState = () => { called = true; };
161
+
162
+ try {
163
+ NavigationManager.updateLocaleInUrl();
164
+ expect(called).toBe(false);
165
+ } finally {
166
+ window.history.pushState = originalPushState;
167
+ CONFIG.i18n = false;
168
+ I18nService['_currentLanguage'] = 'en';
169
+ window.history.pushState({}, '', '/');
170
+ }
171
+ });
172
+
173
+ it('matchPattern matches a required param', () => {
174
+ expect(NavigationManager.matchPattern('/posts/:id', '/posts/123')).toEqual({ id: '123' });
175
+ });
176
+
177
+ it('matchPattern does not match when a required param segment is missing', () => {
178
+ expect(NavigationManager.matchPattern('/posts/:id', '/posts')).toBe(null);
179
+ });
180
+
181
+ it('matchPattern matches an optional param when present, and omits it when absent', () => {
182
+ expect(NavigationManager.matchPattern('/users/:id?', '/users/123')).toEqual({ id: '123' });
183
+ expect(NavigationManager.matchPattern('/users/:id?', '/users')).toEqual({});
184
+ });
185
+
186
+ it('matchPattern matches multiple params in the same pattern', () => {
187
+ expect(NavigationManager.matchPattern('/posts/:postId/comments/:commentId', '/posts/1/comments/2'))
188
+ .toEqual({ postId: '1', commentId: '2' });
189
+ });
190
+
191
+ it('matchPattern returns null when the path does not match the pattern at all', () => {
192
+ expect(NavigationManager.matchPattern('/posts/:id', '/other/123')).toBe(null);
193
+ });
194
+
195
+ it('updateDocumentTitle falls back to the view\'s viewName when generateSEOFiles is disabled', () => {
196
+ CONFIG.generateSEOFiles = false;
197
+ CONFIG.seo = { routes: [{ path: '/', title: 'Should not be used' }] };
198
+ try {
199
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
200
+ expect(document.title).toBe('home');
201
+ } finally {
202
+ CONFIG.generateSEOFiles = false;
203
+ }
204
+ });
205
+
206
+ it('updateDocumentTitle falls back to the view\'s viewName when no seo.json route matches the pattern', () => {
207
+ CONFIG.generateSEOFiles = true;
208
+ CONFIG.seo = { routes: [{ path: '/other', title: 'Other' }] };
209
+ try {
210
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
211
+ expect(document.title).toBe('home');
212
+ } finally {
213
+ CONFIG.generateSEOFiles = false;
214
+ }
215
+ });
216
+
217
+ it('updateDocumentTitle uses a flat seo.json title when generateSEOFiles is enabled and the pattern matches', () => {
218
+ CONFIG.generateSEOFiles = true;
219
+ CONFIG.seo = { routes: [{ path: '/', title: 'My App — Home' }] };
220
+ try {
221
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
222
+ expect(document.title).toBe('My App — Home');
223
+ } finally {
224
+ CONFIG.generateSEOFiles = false;
225
+ }
226
+ });
227
+
228
+ it('updateDocumentTitle resolves a per-language seo.json title to the current language', () => {
229
+ CONFIG.generateSEOFiles = true;
230
+ CONFIG.seo = { routes: [{ path: '/', title: { en: 'Home', fr: 'Accueil' } }] };
231
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
232
+ I18nService['_currentLanguage'] = 'fr';
233
+ try {
234
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
235
+ expect(document.title).toBe('Accueil');
236
+ } finally {
237
+ CONFIG.generateSEOFiles = false;
238
+ I18nService['_currentLanguage'] = 'en';
239
+ }
240
+ });
241
+
242
+ it('updateDocumentTitle falls back to the default language, then any available language, then viewName', () => {
243
+ CONFIG.generateSEOFiles = true;
244
+ I18nService['_LANGUAGES'] = ['en', 'fr'];
245
+
246
+ CONFIG.seo = { routes: [{ path: '/', title: { fr: 'Accueil' } }] };
247
+ I18nService['_currentLanguage'] = 'en';
248
+ try {
249
+ // current lang ('en') missing -> falls back to default lang's value, present here as 'fr' only,
250
+ // so it falls further to the first available value
251
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
252
+ expect(document.title).toBe('Accueil');
253
+
254
+ CONFIG.seo = { routes: [{ path: '/', title: {} }] };
255
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
256
+ expect(document.title).toBe('home');
257
+ } finally {
258
+ CONFIG.generateSEOFiles = false;
259
+ I18nService['_currentLanguage'] = 'en';
260
+ }
261
+ });
262
+
263
+ it('updateDocumentTitle uses the view\'s locale title when i18n is enabled and no seo.json route matches', () => {
264
+ CONFIG.i18n = true;
265
+ CONFIG.generateSEOFiles = false;
266
+ I18nService['_translations'] = { home: { title: 'Accueil' } };
267
+ try {
268
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
269
+ expect(document.title).toBe('Accueil');
270
+ } finally {
271
+ CONFIG.i18n = false;
272
+ I18nService['_translations'] = {};
273
+ }
274
+ });
275
+
276
+ it('updateDocumentTitle prefers seo.json title over the locale title when both are present', () => {
277
+ CONFIG.i18n = true;
278
+ CONFIG.generateSEOFiles = true;
279
+ CONFIG.seo = { routes: [{ path: '/', title: 'From seo.json' }] };
280
+ I18nService['_translations'] = { home: { title: 'From locale' } };
281
+ try {
282
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
283
+ expect(document.title).toBe('From seo.json');
284
+ } finally {
285
+ CONFIG.i18n = false;
286
+ CONFIG.generateSEOFiles = false;
287
+ I18nService['_translations'] = {};
288
+ }
289
+ });
290
+
291
+ it('updateDocumentTitle ignores the locale title when i18n is disabled, falling back to viewName', () => {
292
+ CONFIG.i18n = false;
293
+ CONFIG.generateSEOFiles = false;
294
+ I18nService['_translations'] = { home: { title: 'Should not be used' } };
295
+ try {
296
+ NavigationManager.updateDocumentTitle({ viewName: 'home' }, '/');
297
+ expect(document.title).toBe('home');
298
+ } finally {
299
+ I18nService['_translations'] = {};
300
+ }
301
+ });
51
302
  });
@@ -1,13 +1,48 @@
1
- import { {{#if i18n}}I18nService, {{/if}}View } from "../../../index.js";
1
+ import { I18nService, View } from "../../../index.js";
2
+ import { Language } from "../../i18n/languages.js";
3
+ import { CONFIG } from "../../../config.js";
2
4
 
3
5
  /**
4
6
  * Navigation - handles path normalization and history management
5
7
  */
6
8
  export class NavigationManager {
9
+ public static extractLocale(path: string): { locale: Language | null; strippedPath: string } {
10
+ if (!CONFIG.i18n) return { locale: null, strippedPath: path };
11
+
12
+ const segments = path.split('/').filter(Boolean);
13
+ const first = segments[0] as Language;
14
+ if (first && I18nService.languages.includes(first)) {
15
+ const rest = segments.slice(1).join('/');
16
+ return { locale: first, strippedPath: rest ? `/${rest}` : '/' };
17
+ }
18
+ return { locale: null, strippedPath: path };
19
+ }
20
+
21
+ public static addLocalePrefix(strippedPath: string): string {
22
+ if (!CONFIG.i18n) return strippedPath;
23
+
24
+ const lang = I18nService.currentLanguage;
25
+ return strippedPath === '/' ? `/${lang}` : `/${lang}${strippedPath}`;
26
+ }
27
+
28
+ public static getCurrentLocale(): Language | null {
29
+ return this.extractLocale(window.location.pathname).locale;
30
+ }
31
+
32
+ public static updateLocaleInUrl(): void {
33
+ const rawPathname = (new URL(window.location.pathname, window.location.origin).pathname || '/').replace(/\/+$/, '') || '/';
34
+ const { strippedPath } = this.extractLocale(rawPathname);
35
+ const newUrl = this.addLocalePrefix(strippedPath);
36
+ if (newUrl !== window.location.pathname) {
37
+ window.history.pushState({}, '', newUrl);
38
+ }
39
+ }
40
+
7
41
  public static normalizePath(path: string): string {
8
42
  const collapsed = path.replace(/\/\/+/g, '/');
9
43
  const url = new URL(collapsed, window.location.origin);
10
- return (url.pathname || '/').replace(/\/+$/, '') || '/';
44
+ const pathname = (url.pathname || '/').replace(/\/+$/, '') || '/';
45
+ return this.extractLocale(pathname).strippedPath;
11
46
  }
12
47
 
13
48
  public static updateHistory(
@@ -15,32 +50,32 @@ export class NavigationManager {
15
50
  currentPath: string,
16
51
  pushState: boolean
17
52
  ): void {
18
- if (this.shouldPushState(pushState, currentPath, normalizedPath)) {
19
- window.history.pushState({}, '', normalizedPath);
53
+ const localizedPath = this.addLocalePrefix(normalizedPath);
54
+ if (pushState && window.location.pathname !== localizedPath) {
55
+ window.history.pushState({}, '', localizedPath);
20
56
  }
21
57
  }
22
58
 
23
- public static updateMetaContent(currentView: View): void {
24
- if (!currentView.shouldUpdateMetaContent()) return;
25
-
26
- const strippedName = currentView.viewName.replace('-view', '');
27
- {{#if i18n}}
28
- const title = `${strippedName}.meta.title`;
29
- const description = `${strippedName}.meta.description`;
30
-
31
- document.title = I18nService.translate(title);
32
- document.querySelector('meta[name="description"]')
33
- ?.setAttribute('content', I18nService.translate(description));
34
- {{else}}
35
-
36
- document.title = strippedName.charAt(0).toUpperCase() + strippedName.slice(1).toLowerCase();
37
- {{/if}}
38
- }
39
-
40
59
  public static getCurrentPath(): string {
41
60
  return this.normalizePath(window.location.pathname);
42
61
  }
43
62
 
63
+ public static updateDocumentTitle(view: View, pattern: string): void {
64
+ const route = CONFIG.generateSEOFiles
65
+ ? CONFIG.seo?.routes?.find((r) => r.path === pattern)
66
+ : undefined;
67
+ const seoTitle = route?.title ? this.resolveSeoTitle(route.title) : undefined;
68
+ const localeTitle = CONFIG.i18n ? I18nService.getTranslationObject<string>(`${view.viewName}.title`) : null;
69
+ document.title = seoTitle || localeTitle || view.viewName;
70
+ }
71
+
72
+ private static resolveSeoTitle(title: string | Record<string, string>): string | undefined {
73
+ if (typeof title !== 'object') return title;
74
+ const lang = I18nService.currentLanguage;
75
+ const defaultLang = I18nService.defaultLanguage;
76
+ return title[lang] ?? title[defaultLang] ?? Object.values(title)[0];
77
+ }
78
+
44
79
  public static matchPattern(pattern: string, path: string): Record<string, string> | null {
45
80
  const paramNames: string[] = [];
46
81
 
@@ -68,12 +103,4 @@ export class NavigationManager {
68
103
  });
69
104
  return params;
70
105
  }
71
-
72
- private static shouldPushState(
73
- pushState: boolean,
74
- currentPath: string,
75
- normalizedPath: string
76
- ): boolean {
77
- return pushState && currentPath !== normalizedPath;
78
- }
79
106
  }
@@ -29,6 +29,12 @@ describe('RouteGuardsManager', () => {
29
29
  expect(JSON.stringify(result)).toBe(JSON.stringify([guard]));
30
30
  });
31
31
 
32
+ it('should default to an empty guards array when an object route config has no guards', () => {
33
+ const config = { view: mockViewConstructor };
34
+ const result = RouteGuardsManager.getRouteGuards(config);
35
+ expect(result).toEqual([]);
36
+ });
37
+
32
38
  it('should pass all guards and return true from runGuards', async () => {
33
39
  const guards = [() => true, () => Promise.resolve(true)];
34
40
  const result = await RouteGuardsManager.runGuards(guards);
@@ -70,4 +76,25 @@ describe('RouteGuardsManager', () => {
70
76
  expect(typeof result.viewConstructor).toBe('function');
71
77
  expect(result.redirectTo).toBeUndefined();
72
78
  });
79
+
80
+ it('passes the actual route params through processRouteGuards to each guard', async () => {
81
+ const receivedParams = [];
82
+ const guard = (params) => { receivedParams.push(params); return true; };
83
+ const config = { view: mockViewConstructor, guards: [guard] };
84
+
85
+ await RouteGuardsManager.processRouteGuards(config, '/users/42', { id: '42' });
86
+
87
+ expect(receivedParams.length).toBe(1);
88
+ expect(receivedParams[0]).toEqual({ id: '42' });
89
+ });
90
+
91
+ it('defaults params to an empty object when none are passed to processRouteGuards', async () => {
92
+ const receivedParams = [];
93
+ const guard = (params) => { receivedParams.push(params); return true; };
94
+ const config = { view: mockViewConstructor, guards: [guard] };
95
+
96
+ await RouteGuardsManager.processRouteGuards(config, '/dashboard');
97
+
98
+ expect(receivedParams[0]).toEqual({});
99
+ });
73
100
  });
@@ -26,24 +26,11 @@ describe('ViewRenderManager', () => {
26
26
  return el;
27
27
  },
28
28
  destroy: () => {},
29
+ onEnter: () => {},
30
+ onExit: () => {},
29
31
  };
30
32
  };
31
33
 
32
- {{#if animation}}
33
- it('should add exit class and destroy current view on transition out', async () => {
34
- const view = mockView();
35
- const el = view.render();
36
- container.appendChild(el);
37
-
38
- // Spy on destroy
39
- const spy = spyOn(view, 'destroy');
40
-
41
- await ViewRenderManager.transitionOutCurrentView(view);
42
-
43
- expect(spy).toHaveBeenCalled();
44
- });
45
-
46
- {{/if}}
47
34
  it('should do nothing if no current view is passed to transitionOutCurrentView', async () => {
48
35
  const result = await ViewRenderManager.transitionOutCurrentView(null);
49
36
  expect(result).toBe(null);
@@ -51,7 +38,8 @@ describe('ViewRenderManager', () => {
51
38
 
52
39
  it('should emit view-unmount event on transition out', async () => {
53
40
  const emitted = [];
54
- AppEventBus.emit = (event, data) => emitted.push({ event, data });
41
+ const emitSpy = spyOn(AppEventBus, 'emit');
42
+ emitSpy.andCallFake((event, data) => emitted.push({ event, data }));
55
43
 
56
44
  const view = mockView('SettingsView');
57
45
  const el = view.render();
@@ -59,6 +47,84 @@ describe('ViewRenderManager', () => {
59
47
 
60
48
  await ViewRenderManager.transitionOutCurrentView(view);
61
49
 
62
- expect(JSON.stringify(emitted)).toContain(JSON.stringify({ event: 'view-unmount', data: 'SettingsView' }));
50
+ expect(JSON.stringify(emitted)).toContain(JSON.stringify({ event: 'view-unmount', data: { viewName: 'SettingsView' } }));
51
+ emitSpy.restore();
52
+ });
53
+
54
+ it('should call destroy() and onExit() on the current view when transitioning out', async () => {
55
+ let destroyed = false;
56
+ let exited = false;
57
+ const view = mockView('SettingsView');
58
+ view.destroy = () => { destroyed = true; };
59
+ view.onExit = () => { exited = true; };
60
+
61
+ await ViewRenderManager.transitionOutCurrentView(view);
62
+
63
+ expect(destroyed).toBe(true);
64
+ expect(exited).toBe(true);
65
+ });
66
+
67
+ it('should set route params, render, call onEnter and emit view-mount when rendering a new view', () => {
68
+ const emitted = [];
69
+ const emitSpy = spyOn(AppEventBus, 'emit');
70
+ emitSpy.andCallFake((event, data) => emitted.push({ event, data }));
71
+
72
+ const calls = [];
73
+ const viewElement = document.createElement('div');
74
+ const view = {
75
+ viewName: 'HomeView',
76
+ getElement: () => viewElement,
77
+ setRouteParams: (params) => calls.push(['setRouteParams', params]),
78
+ render: () => { calls.push(['render']); return viewElement; },
79
+ onEnter: () => calls.push(['onEnter']),
80
+ };
81
+
82
+ ViewRenderManager.renderNewView(() => view, { id: '42' });
83
+
84
+ expect(calls).toEqual([
85
+ ['setRouteParams', { id: '42' }],
86
+ ['render'],
87
+ ['onEnter'],
88
+ ]);
89
+ expect(JSON.stringify(emitted)).toContain(JSON.stringify({ event: 'view-mount', data: { viewName: 'HomeView' } }));
90
+ emitSpy.restore();
91
+ });
92
+
93
+ it('should not attempt to clear stale content when the new view has no parent element yet', () => {
94
+ const viewElement = document.createElement('div');
95
+ // Note: viewElement is intentionally never attached to the container.
96
+ const view = {
97
+ viewName: 'HomeView',
98
+ getElement: () => viewElement,
99
+ setRouteParams: () => {},
100
+ render: () => viewElement,
101
+ onEnter: () => {},
102
+ };
103
+
104
+ expect(() => ViewRenderManager.renderNewView(() => view)).not.toThrow();
105
+ });
106
+
107
+ it('should clear stale mount content before rendering the new view', () => {
108
+ const stale = document.createElement('div');
109
+ stale.className = 'stale-seo-content';
110
+ container.appendChild(stale);
111
+
112
+ const viewElement = document.createElement('div');
113
+ viewElement.className = 'mock-view';
114
+ container.appendChild(viewElement);
115
+
116
+ const view = {
117
+ viewName: 'HomeView',
118
+ getElement: () => viewElement,
119
+ setRouteParams: () => {},
120
+ render: () => viewElement,
121
+ onEnter: () => {},
122
+ };
123
+
124
+ ViewRenderManager.renderNewView(() => view);
125
+
126
+ expect(container.children.length).toBe(1);
127
+ expect(container.contains(stale)).toBe(false);
128
+ expect(container.contains(viewElement)).toBe(true);
63
129
  });
64
130
  });
@@ -1,35 +1,14 @@
1
- import { View, AppEventBus } from '../../../index.js';
1
+ import { View, Lifecycle } from '../../../index.js';
2
2
 
3
3
  /**
4
- * handles all view rendering{{#if transition}} and transition animation logic{{/if}}.
4
+ * handles all view rendering.
5
5
  */
6
6
  export class ViewRenderManager {
7
- {{#if transition}}
8
- public static renderAnimatedView(view: View): void {
9
- const container = document.getElementById('app');
10
- const el = view.render();
11
- el.classList.add('app-view-transition');
12
- container?.appendChild(el);
13
-
14
- requestAnimationFrame(() => {
15
- el.classList.add('enter-active');
16
- });
17
- }
18
-
19
- {{/if}}
20
7
  public static async transitionOutCurrentView(currentView: View | null): Promise<null> {
21
8
  if (!currentView) return null;
22
- {{#if transition}}
23
- const container = document.getElementById('app');
24
- const el = container?.firstElementChild as HTMLElement;
25
-
26
- if (el) {
27
- el.classList.add('exit-active');
28
- await this.delay(300); // match transition duration in _libs.scss
29
- }
30
- {{/if}}
31
9
  currentView.destroy();
32
- this.emitEvent('view-unmount', currentView.viewName);
10
+ currentView.onExit();
11
+ Lifecycle.viewUnmount(currentView.viewName);
33
12
  return null;
34
13
  }
35
14
 
@@ -38,28 +17,25 @@ export class ViewRenderManager {
38
17
  params: Record<string, string> = {}
39
18
  ): View {
40
19
  const view = viewConstructor();
20
+ ViewRenderManager.clearStaleMountContent(view);
41
21
 
42
22
  // Set route parameters before rendering
43
23
  view.setRouteParams(params);
44
24
 
45
- {{#if transition}}
46
- this.renderAnimatedView(view);
47
-
48
- {{else}}
49
25
  view.render();
26
+ view.onEnter();
50
27
 
51
- {{/if}}
52
- this.emitEvent('view-mount', view.viewName);
28
+ Lifecycle.viewMount(view.viewName);
53
29
  return view;
54
30
  }
55
31
 
56
- private static emitEvent(event: EventKey, viewName: string): void {
57
- AppEventBus.emit(event, viewName);
58
- }
59
- {{#if transition}}
32
+ private static clearStaleMountContent(view: View): void {
33
+ const viewElement = view.getElement();
34
+ const container = viewElement.parentElement;
35
+ if (!container) return;
60
36
 
61
- private static delay(ms: number): Promise<void> {
62
- return new Promise(res => setTimeout(res, ms));
37
+ Array.from(container.childNodes).forEach((node) => {
38
+ if (node !== viewElement) node.remove();
39
+ });
63
40
  }
64
- {{/if}}
65
41
  }