@nutin/cli 1.3.1 → 2.0.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 (287) hide show
  1. package/CHANGELOG.md +89 -9
  2. package/README.md +20 -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 +14 -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 +136 -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 +169 -0
  36. package/templates/base/GETTING_STARTED.md.hbs +227 -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 +51 -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 +7 -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 +1 -56
  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 +4 -37
  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 +74 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +1 -1
  57. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +145 -0
  58. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +7 -1
  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 +82 -0
  63. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +142 -0
  64. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +67 -22
  65. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +183 -0
  66. package/templates/base/src/core/base-classes/component/component.test.js.hbs +85 -0
  67. package/templates/base/src/core/base-classes/component/component.ts.hbs +24 -33
  68. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +58 -0
  69. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +75 -0
  70. package/templates/base/src/core/base-classes/service/service.test.js.hbs +150 -0
  71. package/templates/base/src/core/base-classes/service/service.ts.hbs +26 -20
  72. package/templates/base/src/core/base-classes/view/view.test.js.hbs +74 -0
  73. package/templates/base/src/core/base-classes/view/view.ts.hbs +17 -41
  74. package/templates/base/src/core/config.ts.hbs +7 -0
  75. package/templates/base/src/core/index.ts.hbs +0 -3
  76. package/templates/base/src/core/internals.d.ts.hbs +27 -0
  77. package/templates/base/src/core/nutin-config.d.ts.hbs +4 -0
  78. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +67 -3
  79. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +9 -4
  80. package/templates/base/src/core/services/event-bus/index.ts.hbs +3 -0
  81. package/templates/base/src/core/services/event-bus/lifecycle.facade.test.js.hbs +123 -0
  82. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +61 -0
  83. package/templates/base/src/core/services/event-bus/navigation.facade.test.js.hbs +46 -0
  84. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +25 -0
  85. package/templates/base/src/core/services/globals/globals.test.js.hbs +149 -0
  86. package/templates/base/src/core/services/globals/globals.ts.hbs +95 -0
  87. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +25 -0
  88. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +20 -0
  89. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +43 -0
  90. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  91. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +22 -4
  92. package/templates/base/src/core/services/index.ts.hbs +5 -6
  93. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  94. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  95. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  96. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  97. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +149 -6
  98. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +40 -29
  99. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  100. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  101. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +14 -38
  102. package/templates/base/src/core/services/router/router.test.js.hbs +226 -2
  103. package/templates/base/src/core/services/router/router.ts.hbs +40 -8
  104. package/templates/base/src/index.html.hbs +2 -6
  105. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  106. package/templates/base/src/styles/base/_variables.scss.hbs +1 -86
  107. package/templates/base/src/styles/main.scss.hbs +1 -18
  108. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  109. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  110. package/templates/base/tools/builder/builder.js.hbs +24 -26
  111. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  112. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  113. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  114. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  115. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  116. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  117. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  118. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  119. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  120. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  121. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  122. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  123. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  124. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  125. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  126. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  127. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  128. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  129. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  130. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  131. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  132. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  133. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  134. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  136. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  137. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  138. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  139. package/templates/base/tools/dev/serve.js.hbs +11 -1
  140. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  141. package/templates/base/tools/generator/generator.js.hbs +64 -47
  142. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  143. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  144. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  145. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  146. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  147. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  148. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  149. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +4 -0
  150. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +7 -0
  151. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  152. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  153. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  154. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  155. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  156. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  157. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +3 -2
  158. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  159. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  160. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  164. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  165. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  166. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  167. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  168. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  169. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  170. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  171. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  172. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  173. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  174. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  175. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  176. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  177. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  178. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  179. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  180. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  181. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  182. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  183. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  185. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  186. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  187. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  188. package/templates/base/tools/utils/format.js.hbs +10 -10
  189. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  190. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  191. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  192. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  193. package/templates/base/tools/utils/index.js.hbs +1 -3
  194. package/templates/base/tools/utils/languages.js.hbs +15 -0
  195. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  196. package/templates/base/tools/utils/print.js.hbs +19 -13
  197. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  198. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  199. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  200. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  201. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  202. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  203. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  204. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  205. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  206. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  207. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  208. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  209. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  211. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  212. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  213. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  214. package/lib/src/context-builder.mjs +0 -40
  215. package/lib/src/file-generator.mjs +0 -109
  216. package/lib/src/json-generator.mjs +0 -31
  217. package/lib/src/package-manager.mjs +0 -139
  218. package/lib/src/print.mjs +0 -32
  219. package/lib/src/utils.mjs +0 -18
  220. package/lib/src/version.mjs +0 -1
  221. package/templates/base/builder.config.js.hbs +0 -36
  222. package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +0 -32
  223. package/templates/base/src/app/components/common/button/button.component.ts.hbs +0 -40
  224. package/templates/base/src/app/dom.test.js.hbs +0 -13
  225. package/templates/base/src/app/services/example/example.service.ts.hbs +0 -18
  226. package/templates/base/src/app/views/home/home.view.test.js.hbs +0 -26
  227. package/templates/base/src/core/services/store/store.test.js.hbs +0 -8
  228. package/templates/base/src/core/services/store/store.ts.hbs +0 -39
  229. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.test.js.hbs +0 -0
  230. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +0 -113
  231. package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +0 -249
  232. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +0 -131
  233. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +0 -50
  234. package/templates/base/src/core/utils/index.ts.hbs +0 -2
  235. package/templates/base/src/libs/index.ts.hbs +0 -4
  236. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +0 -197
  237. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +0 -75
  238. package/templates/base/src/libs/popover/popover.ts.hbs +0 -138
  239. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -75
  240. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  241. package/templates/base/src/styles/core/_anchors.scss.hbs +0 -10
  242. package/templates/base/src/styles/core/_index.scss.hbs +0 -6
  243. package/templates/base/src/styles/core/_popover.scss.hbs +0 -96
  244. package/templates/base/src/styles/core/_snackbar.scss.hbs +0 -50
  245. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  246. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  247. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  248. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  249. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  250. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  251. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  252. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  253. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  254. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  255. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  256. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  257. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  258. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  259. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  260. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  261. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  262. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  263. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -254
  264. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  265. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  266. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  267. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  268. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  269. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  270. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  271. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  272. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  273. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  274. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  275. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  276. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  277. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  278. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  279. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  280. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  281. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  282. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  283. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  284. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  285. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  286. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  287. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Abstract class for implementing the Singleton pattern.
2
+ * Abstract class for implementing the Singleton pattern.
3
3
  * Supports constructor parameters for initialization.
4
4
  */
5
5
  export abstract class Service<T extends Service<T>> {
6
6
  private static _instances = new Map<Function, any>();
7
7
  private static _instantiating = new Set<Function>();
8
8
  private _cleanupCallbacks: (() => void)[] = [];
9
-
9
+
10
10
  constructor(...args: any[]) {
11
11
  const constructor = this.constructor as new (...args: any[]) => T;
12
-
12
+
13
13
  if (!Service._instantiating.has(constructor)) {
14
14
  throw new Error(
15
15
  `${constructor.name} is a Service. Use ${constructor.name}.getInstance() instead.`
@@ -19,31 +19,39 @@ export abstract class Service<T extends Service<T>> {
19
19
  window.addEventListener('beforeunload', this.dispose);
20
20
  }
21
21
 
22
- public static createInstance<T extends Service<T>>(
23
- constructor: new (...args: any[]) => T,
22
+ /**
23
+ * Returns the singleton instance, creating it with the provided args if it
24
+ * doesn't exist yet. Throws if args are passed to an already-initialized instance,
25
+ * preventing silent re-initialization mismatches.
26
+ */
27
+ public static getInstance<T extends Service<T>>(
28
+ this: new (...args: any[]) => T,
24
29
  ...args: any[]
25
30
  ): T {
26
- if (!Service._instances.has(constructor)) {
27
- Service._instantiating.add(constructor);
28
- Service._instances.set(constructor, new constructor(...args));
29
- Service._instantiating.delete(constructor);
31
+ const alreadyExists = Service._instances.has(this);
32
+
33
+ if (alreadyExists && args.length > 0) {
34
+ throw new Error(
35
+ `${this.name} is already initialized. ` +
36
+ `getInstance() was called with arguments on an existing instance, ` +
37
+ `which would silently ignore them. ` +
38
+ `Call getInstance() without arguments to retrieve the existing instance.`
39
+ );
30
40
  }
31
- return Service._instances.get(constructor);
32
- }
33
41
 
34
- public static getInstance<T extends Service<T>>(this: new () => T): T {
35
- if (!Service._instances.has(this)) {
42
+ if (!alreadyExists) {
36
43
  Service._instantiating.add(this);
37
- Service._instances.set(this, new this());
44
+ Service._instances.set(this, new this(...args));
38
45
  Service._instantiating.delete(this);
39
46
  }
47
+
40
48
  return Service._instances.get(this);
41
49
  }
42
50
 
43
51
  public static hasInstance<T extends Service<T>>(
44
52
  constructor: new (...args: any[]) => T
45
53
  ): boolean {
46
- return Service._instances.has(constructor);
54
+ return Service._instances.has(constructor);
47
55
  }
48
56
 
49
57
  public static async destroy<T extends Service<T>>(
@@ -65,7 +73,7 @@ export abstract class Service<T extends Service<T>> {
65
73
  public static async destroyAll(): Promise<void> {
66
74
  const destroyPromises = Array.from(Service._instances.values())
67
75
  .map(instance => instance.onDestroy());
68
-
76
+
69
77
  await Promise.all(destroyPromises);
70
78
  Service._instances.clear();
71
79
  Service._instantiating.clear();
@@ -76,11 +84,9 @@ export abstract class Service<T extends Service<T>> {
76
84
  }
77
85
 
78
86
  /**
79
- * For async and custom cleanup operations
87
+ * For async and custom cleanup operations.
80
88
  */
81
- protected onDestroy<T extends Service<T>>(this: () => T): void {
82
- console.log(`No onDestroy operations.`)
83
- }
89
+ protected onDestroy(): void {}
84
90
 
85
91
  private autoBindMethods(): void {
86
92
  let proto = Object.getPrototypeOf(this);
@@ -0,0 +1,74 @@
1
+ import { View } from '#root/dist/src/core/index.js';
2
+
3
+ class HomeView extends View {}
4
+ class MyFancyView extends View {}
5
+ class NamedView extends View {}
6
+
7
+ describe('View', () => {
8
+ let app;
9
+
10
+ beforeEach(() => {
11
+ app = document.getElementById('app');
12
+ });
13
+
14
+ afterEach(() => {
15
+ app.innerHTML = '';
16
+ app = null;
17
+ });
18
+
19
+ it('derives a kebab-case viewName from the class name, stripping a trailing "View"', () => {
20
+ const view = new HomeView({});
21
+ expect(view.viewName).toBe('home');
22
+ });
23
+
24
+ it('derives a kebab-case viewName for a multi-word class name', () => {
25
+ const view = new MyFancyView({});
26
+ expect(view.viewName).toBe('my-fancy');
27
+ });
28
+
29
+ it('uses an explicit viewName option instead of deriving one', () => {
30
+ const view = new NamedView({ viewName: 'custom-name' });
31
+ expect(view.viewName).toBe('custom-name');
32
+ });
33
+
34
+ it('generateTemplate() returns the constructor template', () => {
35
+ const view = new HomeView({ template: '<p>hi</p>' });
36
+ expect(view.render().innerHTML).toBe('<p>hi</p>');
37
+ });
38
+
39
+ it('defaults the template to an empty string', () => {
40
+ const view = new HomeView({});
41
+ expect(view.render().innerHTML).toBe('');
42
+ });
43
+
44
+ it('defaults the tagName to "section"', () => {
45
+ const view = new HomeView({});
46
+ expect(view.getElement().tagName).toBe('SECTION');
47
+ });
48
+
49
+ it('setRouteParams/getRouteParams/getRouteParam/hasRouteParam manage route params', () => {
50
+ const view = new HomeView({});
51
+ expect(view.hasRouteParam('id')).toBe(false);
52
+ expect(view.getRouteParam('id')).toBeUndefined();
53
+
54
+ view.setRouteParams({ id: '42' });
55
+
56
+ expect(view.hasRouteParam('id')).toBe(true);
57
+ expect(view.getRouteParam('id')).toBe('42');
58
+ expect(view.getRouteParams()).toEqual({ id: '42' });
59
+ });
60
+
61
+ it('getRouteParams() returns a copy, not a live reference', () => {
62
+ const view = new HomeView({});
63
+ view.setRouteParams({ id: '1' });
64
+ const params = view.getRouteParams();
65
+ params.id = 'mutated';
66
+ expect(view.getRouteParam('id')).toBe('1');
67
+ });
68
+
69
+ it('onEnter and onExit are present as no-op hooks', () => {
70
+ const view = new HomeView({});
71
+ expect(() => view.onEnter()).not.toThrow();
72
+ expect(() => view.onExit()).not.toThrow();
73
+ });
74
+ });
@@ -7,71 +7,47 @@ export interface ViewOptions extends BaseComponentOptions {
7
7
  export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComponent<T> {
8
8
  public viewName: string;
9
9
  protected routeParams: Record<string, string> = {};
10
+ private _template: string;
10
11
 
11
12
  constructor({
12
- template,
13
- tagName = 'section',
14
- mountTarget = '#app',
15
- viewName
16
- }: ViewOptions) {
17
- super({template, mountTarget, tagName});
13
+ template,
14
+ tagName = 'section',
15
+ mountTarget = '#app',
16
+ viewName,
17
+ trustLevel,
18
+ }: ViewOptions) {
19
+ super({ mountTarget, tagName, trustLevel });
20
+ this._template = template ?? '';
18
21
  this.viewName = viewName || this.getKebabCaseViewName();
19
22
  }
20
23
 
21
- /**
22
- * Set route parameters for this view instance
23
- * Called by the router when the view is instantiated
24
- */
24
+ protected override generateTemplate(): string {
25
+ return this._template;
26
+ }
27
+
25
28
  public setRouteParams(params: Record<string, string>): void {
26
29
  this.routeParams = { ...params };
27
30
  }
28
31
 
29
- /**
30
- * Get all route parameters
31
- */
32
32
  public getRouteParams(): Record<string, string> {
33
33
  return { ...this.routeParams };
34
34
  }
35
35
 
36
- /**
37
- * Get a specific route parameter
38
- */
39
36
  public getRouteParam(key: string): string | undefined {
40
37
  return this.routeParams[key];
41
38
  }
42
39
 
43
- /**
44
- * Check if a route parameter exists and has a value
45
- */
46
40
  public hasRouteParam(key: string): boolean {
47
41
  return key in this.routeParams && this.routeParams[key] !== undefined;
48
42
  }
49
43
 
50
- public shouldUpdateMetaContent(): boolean {
51
- return true;
52
- }
53
-
54
- public onEnter(): void {
55
- console.log(`${this.viewName} mounted with params:`, this.routeParams);
56
- }
57
-
58
- public onExit(): void {
59
- console.log(`${this.viewName} unmounted`);
60
- }
61
-
62
- public override render(): HTMLElement {
63
- this.onEnter();
64
- return super.render();
65
- }
66
-
67
- public override destroy(): void {
68
- super.destroy();
69
- this.onExit();
70
- }
71
-
72
44
  private getKebabCaseViewName(): string {
73
45
  const className = this.constructor.name;
74
46
  const baseName = className.replace(/View$/, '');
75
47
  return baseName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
76
48
  }
49
+
50
+ // Navigation hooks — called by router only, never by render lifecycle
51
+ public onEnter(): void {}
52
+ public onExit(): void {}
77
53
  }
@@ -0,0 +1,7 @@
1
+ import langs from '../../config/languages.json' with { type: 'json' };
2
+ import nutinConfig from '../../nutin.config.js';
3
+
4
+ export const CONFIG = {
5
+ langs,
6
+ i18n: nutinConfig.i18n,
7
+ } as const;
@@ -3,6 +3,3 @@ export * from './base-classes/index.js';
3
3
 
4
4
  // Core functionalities
5
5
  export * from './services/index.js';
6
-
7
- // Helper functions
8
- export * from './utils/index.js';
@@ -0,0 +1,27 @@
1
+ declare interface NavigationEventMap {
2
+ 'navigate': { path: string };
3
+ 'reload': {};
4
+ }
5
+
6
+ declare interface LifecycleEventMap {
7
+ 'before-render': {};
8
+ 'after-render': {};
9
+ 'before-destroy': {};
10
+ 'after-destroy': {};
11
+ 'view-mount': { viewName: string };
12
+ 'view-unmount': { viewName: string };
13
+ }
14
+
15
+ declare interface I18nEventMap {
16
+ 'language-changed': {
17
+ lang: string;
18
+ };
19
+ }
20
+
21
+ declare interface FrameworkEventMap extends NavigationEventMap, LifecycleEventMap, I18nEventMap {}
22
+
23
+ declare interface AppEventMap {}
24
+
25
+ declare interface EventMap extends FrameworkEventMap, AppEventMap {}
26
+
27
+ declare type EventKey = keyof EventMap;
@@ -0,0 +1,4 @@
1
+ declare module '*/nutin.config.js' {
2
+ const nutinConfig: { i18n: boolean; [key: string]: unknown };
3
+ export default nutinConfig;
4
+ }
@@ -127,11 +127,75 @@ describe('EventBus', () => {
127
127
  });
128
128
  it('should reset all service instances', () => {
129
129
  const eventBus1 = EventBus.getInstance();
130
-
130
+
131
131
  EventBus.testingResetAll();
132
-
132
+
133
133
  const eventBus2 = EventBus.getInstance();
134
-
134
+
135
135
  expect(eventBus1).not.toBe(eventBus2);
136
136
  });
137
+
138
+ it('should only invoke a once() handler on the first emit', () => {
139
+ let callCount = 0;
140
+ eventBus.once('user-login', () => { callCount++; });
141
+
142
+ eventBus.emit('user-login', { userId: '1' });
143
+ expect(callCount).toBe(1);
144
+
145
+ eventBus.emit('user-login', { userId: '2' });
146
+ expect(callCount).toBe(1);
147
+ });
148
+
149
+ it('should auto-unsubscribe a once() handler from _subscriptions after it fires', () => {
150
+ eventBus.once('user-login', () => {});
151
+ expect(eventBus._subscriptions.length).toBe(1);
152
+
153
+ eventBus.emit('user-login', {});
154
+ expect(eventBus._subscriptions.length).toBe(0);
155
+ });
156
+
157
+ it('should not remove a once() subscription before it has fired', () => {
158
+ eventBus.once('user-login', () => {});
159
+ eventBus.emit('notification', {});
160
+ expect(eventBus._subscriptions.length).toBe(1);
161
+ });
162
+
163
+ it('should let a once() handler and a regular subscriber on the same event coexist', () => {
164
+ let onceCalls = 0;
165
+ let regularCalls = 0;
166
+ eventBus.once('notification', () => { onceCalls++; });
167
+ eventBus.subscribe('notification', () => { regularCalls++; });
168
+
169
+ eventBus.emit('notification', {});
170
+ expect(onceCalls).toBe(1);
171
+ expect(regularCalls).toBe(1);
172
+
173
+ eventBus.emit('notification', {});
174
+ expect(onceCalls).toBe(1);
175
+ expect(regularCalls).toBe(2);
176
+ });
177
+
178
+ it('off(event) with no callback removes every handler for that event', () => {
179
+ let callsA = 0;
180
+ let callsB = 0;
181
+ eventBus.subscribe('notification', () => { callsA++; });
182
+ eventBus.subscribe('notification', () => { callsB++; });
183
+
184
+ eventBus.off('notification');
185
+ eventBus.emit('notification', {});
186
+
187
+ expect(callsA).toBe(0);
188
+ expect(callsB).toBe(0);
189
+ expect(eventBus.handlers['notification']).toBeUndefined();
190
+ });
191
+
192
+ it('off(event) with no callback does not affect other events', () => {
193
+ let called = false;
194
+ eventBus.subscribe('user-login', () => { called = true; });
195
+
196
+ eventBus.off('notification');
197
+ eventBus.emit('user-login', {});
198
+
199
+ expect(called).toBe(true);
200
+ });
137
201
  });
@@ -2,7 +2,7 @@ import { Service } from "../../index.js";
2
2
 
3
3
  export type IEventBus = InstanceType<typeof EventBus>;
4
4
 
5
- type Subscription<K extends EventKey = EventKey> = {
5
+ export type Subscription<K extends EventKey = EventKey> = {
6
6
  event: K;
7
7
  callback: (data: EventMap[K]) => void;
8
8
  once?: boolean;
@@ -58,7 +58,10 @@ export class EventBus extends Service<EventBus> {
58
58
  if (!handlers) return;
59
59
 
60
60
  if (callback) {
61
- this.handlers[event] = handlers.filter(h => h !== callback);
61
+ (this.handlers as Record<
62
+ EventKey,
63
+ Array<(data: EventMap[K]) => void>
64
+ >)[event] = handlers.filter(h => h !== callback);
62
65
  } else {
63
66
  delete this.handlers[event];
64
67
  }
@@ -76,8 +79,10 @@ export class EventBus extends Service<EventBus> {
76
79
  if (!this.handlers[event]) {
77
80
  this.handlers[event] = [];
78
81
  }
79
- this.handlers[event]!.push(callback);
80
- this._subscriptions.push({ event, callback, once });
82
+ (
83
+ this.handlers as Record<EventKey, Array<(data: EventMap[K]) => void>>
84
+ )[event].push(callback);
85
+ this._subscriptions.push({ event, callback, once } as Subscription);
81
86
  }
82
87
 
83
88
  private cleanupEventListeners = (): void => {
@@ -0,0 +1,3 @@
1
+ export * from './event-bus.js';
2
+ export * from './lifecycle.facade.js';
3
+ export * from './navigation.facade.js';
@@ -0,0 +1,123 @@
1
+ import { Lifecycle, AppEventBus } from '#root/dist/src/core/index.js';
2
+
3
+ describe('LifecycleFacade (Lifecycle)', () => {
4
+ afterEach(() => {
5
+ AppEventBus.cleanupEventListeners();
6
+ });
7
+
8
+ it('beforeRender emits "before-render"', () => {
9
+ const emit = spyOn(AppEventBus, 'emit');
10
+ Lifecycle.beforeRender();
11
+ expect(emit).toHaveBeenCalledWith('before-render');
12
+ emit.restore();
13
+ });
14
+
15
+ it('onBeforeRender subscribes to "before-render", and the returned unsubscribe removes it', () => {
16
+ let called = 0;
17
+ const unsubscribe = Lifecycle.onBeforeRender(() => called++);
18
+
19
+ Lifecycle.beforeRender();
20
+ expect(called).toBe(1);
21
+
22
+ unsubscribe();
23
+ Lifecycle.beforeRender();
24
+ expect(called).toBe(1);
25
+ });
26
+
27
+ it('afterRender emits "after-render"', () => {
28
+ const emit = spyOn(AppEventBus, 'emit');
29
+ Lifecycle.afterRender();
30
+ expect(emit).toHaveBeenCalledWith('after-render');
31
+ emit.restore();
32
+ });
33
+
34
+ it('onAfterRender subscribes to "after-render", and the returned unsubscribe removes it', () => {
35
+ let called = 0;
36
+ const unsubscribe = Lifecycle.onAfterRender(() => called++);
37
+
38
+ Lifecycle.afterRender();
39
+ expect(called).toBe(1);
40
+
41
+ unsubscribe();
42
+ Lifecycle.afterRender();
43
+ expect(called).toBe(1);
44
+ });
45
+
46
+ it('beforeDestroy emits "before-destroy"', () => {
47
+ const emit = spyOn(AppEventBus, 'emit');
48
+ Lifecycle.beforeDestroy();
49
+ expect(emit).toHaveBeenCalledWith('before-destroy');
50
+ emit.restore();
51
+ });
52
+
53
+ it('onBeforeDestroy subscribes to "before-destroy", and the returned unsubscribe removes it', () => {
54
+ let called = 0;
55
+ const unsubscribe = Lifecycle.onBeforeDestroy(() => called++);
56
+
57
+ Lifecycle.beforeDestroy();
58
+ expect(called).toBe(1);
59
+
60
+ unsubscribe();
61
+ Lifecycle.beforeDestroy();
62
+ expect(called).toBe(1);
63
+ });
64
+
65
+ it('afterDestroy emits "after-destroy"', () => {
66
+ const emit = spyOn(AppEventBus, 'emit');
67
+ Lifecycle.afterDestroy();
68
+ expect(emit).toHaveBeenCalledWith('after-destroy');
69
+ emit.restore();
70
+ });
71
+
72
+ it('onAfterDestroy subscribes to "after-destroy", and the returned unsubscribe removes it', () => {
73
+ let called = 0;
74
+ const unsubscribe = Lifecycle.onAfterDestroy(() => called++);
75
+
76
+ Lifecycle.afterDestroy();
77
+ expect(called).toBe(1);
78
+
79
+ unsubscribe();
80
+ Lifecycle.afterDestroy();
81
+ expect(called).toBe(1);
82
+ });
83
+
84
+ it('viewMount emits "view-mount" with { viewName }', () => {
85
+ const emit = spyOn(AppEventBus, 'emit');
86
+ Lifecycle.viewMount('home');
87
+ expect(emit).toHaveBeenCalledWith('view-mount', { viewName: 'home' });
88
+ emit.restore();
89
+ });
90
+
91
+ it('onViewMount subscribes to "view-mount", and the returned unsubscribe removes it', () => {
92
+ let received = null;
93
+ const unsubscribe = Lifecycle.onViewMount((data) => { received = data; });
94
+
95
+ Lifecycle.viewMount('home');
96
+ expect(received).toEqual({ viewName: 'home' });
97
+
98
+ unsubscribe();
99
+ received = null;
100
+ Lifecycle.viewMount('other');
101
+ expect(received).toBe(null);
102
+ });
103
+
104
+ it('viewUnmount emits "view-unmount" with { viewName }', () => {
105
+ const emit = spyOn(AppEventBus, 'emit');
106
+ Lifecycle.viewUnmount('home');
107
+ expect(emit).toHaveBeenCalledWith('view-unmount', { viewName: 'home' });
108
+ emit.restore();
109
+ });
110
+
111
+ it('onViewUnmount subscribes to "view-unmount", and the returned unsubscribe removes it', () => {
112
+ let received = null;
113
+ const unsubscribe = Lifecycle.onViewUnmount((data) => { received = data; });
114
+
115
+ Lifecycle.viewUnmount('home');
116
+ expect(received).toEqual({ viewName: 'home' });
117
+
118
+ unsubscribe();
119
+ received = null;
120
+ Lifecycle.viewUnmount('other');
121
+ expect(received).toBe(null);
122
+ });
123
+ });
@@ -0,0 +1,61 @@
1
+ import { AppEventBus, IEventBus } from "./event-bus.js";
2
+
3
+ class LifecycleFacade {
4
+ constructor(private bus: IEventBus) {}
5
+
6
+ beforeRender(): void {
7
+ this.bus.emit('before-render');
8
+ }
9
+
10
+ onBeforeRender(callback: () => void): () => void {
11
+ this.bus.subscribe('before-render', callback);
12
+ return () => this.bus.off('before-render', callback);
13
+ }
14
+
15
+ afterRender(): void {
16
+ this.bus.emit('after-render');
17
+ }
18
+
19
+ onAfterRender(callback: () => void): () => void {
20
+ this.bus.subscribe('after-render', callback);
21
+ return () => this.bus.off('after-render', callback);
22
+ }
23
+
24
+ beforeDestroy(): void {
25
+ this.bus.emit('before-destroy');
26
+ }
27
+
28
+ onBeforeDestroy(callback: () => void): () => void {
29
+ this.bus.subscribe('before-destroy', callback);
30
+ return () => this.bus.off('before-destroy', callback);
31
+ }
32
+
33
+ afterDestroy(): void {
34
+ this.bus.emit('after-destroy');
35
+ }
36
+
37
+ onAfterDestroy(callback: () => void): () => void {
38
+ this.bus.subscribe('after-destroy', callback);
39
+ return () => this.bus.off('after-destroy', callback);
40
+ }
41
+
42
+ viewMount(viewName: string): void {
43
+ this.bus.emit('view-mount', {viewName});
44
+ }
45
+
46
+ onViewMount(callback: (data: { viewName: string }) => void): () => void {
47
+ this.bus.subscribe('view-mount', callback);
48
+ return () => this.bus.off('view-mount', callback);
49
+ }
50
+
51
+ viewUnmount(viewName: string): void {
52
+ this.bus.emit('view-unmount', { viewName });
53
+ }
54
+
55
+ onViewUnmount(callback: (data: { viewName: string }) => void): () => void {
56
+ this.bus.subscribe('view-unmount', callback);
57
+ return () => this.bus.off('view-unmount', callback);
58
+ }
59
+ }
60
+
61
+ export const Lifecycle = new LifecycleFacade(AppEventBus);
@@ -0,0 +1,46 @@
1
+ import { Navigation, AppEventBus } from '#root/dist/src/core/index.js';
2
+
3
+ describe('NavigationFacade (Navigation)', () => {
4
+ afterEach(() => {
5
+ AppEventBus.cleanupEventListeners();
6
+ });
7
+
8
+ it('navigateTo emits "navigate" with { path }', () => {
9
+ const emit = spyOn(AppEventBus, 'emit');
10
+ Navigation.navigateTo('/about');
11
+ expect(emit).toHaveBeenCalledWith('navigate', { path: '/about' });
12
+ emit.restore();
13
+ });
14
+
15
+ it('onNavigate subscribes to "navigate", and the returned unsubscribe removes it', () => {
16
+ let received = null;
17
+ const unsubscribe = Navigation.onNavigate((data) => { received = data; });
18
+
19
+ Navigation.navigateTo('/about');
20
+ expect(received).toEqual({ path: '/about' });
21
+
22
+ unsubscribe();
23
+ received = null;
24
+ Navigation.navigateTo('/contact');
25
+ expect(received).toBe(null);
26
+ });
27
+
28
+ it('reload emits "reload"', () => {
29
+ const emit = spyOn(AppEventBus, 'emit');
30
+ Navigation.reload();
31
+ expect(emit).toHaveBeenCalledWith('reload');
32
+ emit.restore();
33
+ });
34
+
35
+ it('onReload subscribes to "reload", and the returned unsubscribe removes it', () => {
36
+ let called = 0;
37
+ const unsubscribe = Navigation.onReload(() => called++);
38
+
39
+ Navigation.reload();
40
+ expect(called).toBe(1);
41
+
42
+ unsubscribe();
43
+ Navigation.reload();
44
+ expect(called).toBe(1);
45
+ });
46
+ });
@@ -0,0 +1,25 @@
1
+ import { AppEventBus, IEventBus } from "./event-bus.js";
2
+
3
+ class NavigationFacade {
4
+ constructor(private bus: IEventBus) {}
5
+
6
+ navigateTo(path: string): void {
7
+ this.bus.emit('navigate', { path });
8
+ }
9
+
10
+ onNavigate(callback: (data: { path: string }) => void): () => void {
11
+ this.bus.subscribe('navigate', callback);
12
+ return () => this.bus.off('navigate', callback);
13
+ }
14
+
15
+ reload(): void {
16
+ this.bus.emit('reload');
17
+ }
18
+
19
+ onReload(callback: () => void): () => void {
20
+ this.bus.subscribe('reload', callback);
21
+ return () => this.bus.off('reload', callback);
22
+ }
23
+ }
24
+
25
+ export const Navigation = new NavigationFacade(AppEventBus);