@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
@@ -197,13 +197,84 @@ describe('HttpClient', () => {
197
197
 
198
198
  it('should handle custom headers', async () => {
199
199
  fetchMock.mockJsonResponse('https://api.example.com/users', []);
200
-
200
+
201
201
  await httpClient.get('https://api.example.com/users', { headers: { 'Authorization': 'Bearer custom-token' } });
202
-
202
+
203
203
  const lastCall = fetchMock.getLastCall();
204
204
  expect(lastCall.options.headers['Authorization']).toBe('Bearer custom-token');
205
205
  });
206
206
 
207
+ it('should send the constructor default headers on a request with no custom headers', async () => {
208
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
209
+
210
+ await httpClient.get('https://api.example.com/users');
211
+
212
+ const lastCall = fetchMock.getLastCall();
213
+ expect(lastCall.options.headers['Content-Type']).toBe('application/json');
214
+ });
215
+
216
+ it('should merge default headers with per-call headers on different keys', async () => {
217
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
218
+
219
+ await httpClient.get('https://api.example.com/users', { headers: { 'Authorization': 'Bearer custom-token' } });
220
+
221
+ const lastCall = fetchMock.getLastCall();
222
+ expect(lastCall.options.headers['Content-Type']).toBe('application/json');
223
+ expect(lastCall.options.headers['Authorization']).toBe('Bearer custom-token');
224
+ });
225
+
226
+ it('should let a per-call header override a default header on the same key', async () => {
227
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
228
+
229
+ await httpClient.get('https://api.example.com/users', { headers: { 'Content-Type': 'text/plain' } });
230
+
231
+ const lastCall = fetchMock.getLastCall();
232
+ expect(lastCall.options.headers['Content-Type']).toBe('text/plain');
233
+ });
234
+
235
+ it('should invoke request interceptors with the final url and options before sending', async () => {
236
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
237
+ const calls = [];
238
+ httpClient.addRequestInterceptor((url, options) => calls.push({ url, options }));
239
+
240
+ await httpClient.get('https://api.example.com/users');
241
+
242
+ expect(calls.length).toBe(1);
243
+ expect(calls[0].url).toBe('https://api.example.com/users');
244
+ expect(calls[0].options.method).toBe('GET');
245
+ });
246
+
247
+ it('should invoke response interceptors with the raw fetch response', async () => {
248
+ fetchMock.mockJsonResponse('https://api.example.com/users', []);
249
+ const received = [];
250
+ httpClient.addResponseInterceptor((response) => received.push(response));
251
+
252
+ await httpClient.get('https://api.example.com/users');
253
+
254
+ expect(received.length).toBe(1);
255
+ expect(received[0].ok).toBe(true);
256
+ });
257
+
258
+ it('should propagate a non-timeout error thrown while parsing the response', async () => {
259
+ fetchMock.mockResponse('https://api.example.com/broken', {
260
+ ok: true,
261
+ status: 200,
262
+ statusText: 'OK',
263
+ headers: { get: () => 'application/json' },
264
+ json: () => Promise.reject(new Error('Malformed JSON')),
265
+ text: () => Promise.resolve('not json'),
266
+ });
267
+
268
+ let thrown = false;
269
+ try {
270
+ await httpClient.get('https://api.example.com/broken');
271
+ } catch (error) {
272
+ thrown = true;
273
+ expect(error.message).toBe('Malformed JSON');
274
+ }
275
+ expect(thrown).toBe(true);
276
+ });
277
+
207
278
  it('should handle HTTP errors', async () => {
208
279
  fetchMock.mockError('https://api.example.com/users', 404, { message: 'Not found' });
209
280
 
@@ -56,7 +56,10 @@ export class HttpClient extends Service<HttpClient> {
56
56
  ): Promise<T> {
57
57
  const { controller, timeoutId } = HttpManager.createAbortController(config?.timeout);
58
58
  const fullUrl = `${this._baseUrl}${endpoint}`;
59
- const mergedConfig = { ...this._defaultHeaders, ...config };
59
+ const mergedConfig: IRequestConfig = {
60
+ ...config,
61
+ headers: { ...this._defaultHeaders, ...config?.headers },
62
+ };
60
63
 
61
64
  try {
62
65
  const url = HttpBuilder.buildRequestUrl(fullUrl, config?.queryParams);
@@ -0,0 +1,270 @@
1
+ import { I18nService, AppEventBus } from '#root/dist/src/core/services/index.js';
2
+ import { CONFIG } from '#root/dist/src/core/config.js';
3
+
4
+ class FetchMock {
5
+ constructor() {
6
+ this.originalFetch = global.fetch;
7
+ this.mockResponses = new Map();
8
+ }
9
+
10
+ mockTranslation(lang, data) {
11
+ this.mockResponses.set(`/locales/${lang}.json`, {
12
+ ok: true,
13
+ status: 200,
14
+ json: () => Promise.resolve(data)
15
+ });
16
+ return this;
17
+ }
18
+
19
+ mockError(url, status = 404) {
20
+ this.mockResponses.set(url, {
21
+ ok: false,
22
+ status,
23
+ json: () => Promise.reject(new Error(`HTTP ${status}`))
24
+ });
25
+ return this;
26
+ }
27
+
28
+ install() {
29
+ global.fetch = (url) => {
30
+ if (this.mockResponses.has(url)) {
31
+ const response = this.mockResponses.get(url);
32
+ return Promise.resolve(response);
33
+ }
34
+
35
+ return this.originalFetch ? this.originalFetch(url) : Promise.reject(new Error('fetch not available'));
36
+ };
37
+ return this;
38
+ }
39
+
40
+ restore() {
41
+ global.fetch = this.originalFetch;
42
+ this.mockResponses.clear();
43
+ }
44
+ }
45
+
46
+ const fetchMock = new FetchMock();
47
+
48
+ describe('i18n module', async () => {
49
+ let originalLanguage;
50
+
51
+ beforeEach(async () => {
52
+ I18nService.resetTranslations();
53
+ fetchMock
54
+ .mockTranslation('en', { home: { title : 'My App', subtitle: "English subtitle" }})
55
+ .mockTranslation('fr', { home: { title : 'Mon App' }})
56
+ .install();
57
+ });
58
+ afterEach(() => {
59
+ fetchMock.restore();
60
+ localStorage.removeItem(I18nService.localStorageKey)
61
+ });
62
+
63
+ beforeAll(() =>{
64
+ originalLanguage = navigator.language;
65
+ Object.defineProperty(navigator, 'language', {
66
+ writable: true,
67
+ configurable: true,
68
+ value: "en-US"
69
+ });
70
+ I18nService['_LANGUAGES'] = [ 'en', 'fr' ];
71
+ I18nService['_DEFAULT_LANGUAGE'] = 'en';
72
+ });
73
+ afterAll(() => {
74
+ Object.defineProperty(navigator, 'language', {
75
+ writable: true,
76
+ configurable: true,
77
+ value: originalLanguage
78
+ });
79
+ });
80
+
81
+ it('should load and use English translations', async () => {
82
+ await I18nService.initTranslations();
83
+ expect(I18nService.currentLanguage).toBe('en');
84
+ expect(I18nService.translate('home.title')).toBe('My App');
85
+ });
86
+
87
+ it('should load and use French translations', async () => {
88
+ Object.defineProperty(navigator, 'language', {
89
+ writable: true,
90
+ configurable: true,
91
+ value: "fr-FR"
92
+ });
93
+ await I18nService.initTranslations();
94
+ expect(I18nService.currentLanguage).toBe('fr');
95
+ expect(I18nService.translate('home.title')).toBe('Mon App');
96
+ });
97
+
98
+ it('should default to English translations', async () => {
99
+ // missing locale
100
+ Object.defineProperty(navigator, 'language', {
101
+ writable: true,
102
+ configurable: true,
103
+ value: "de-DE"
104
+ });
105
+ await I18nService.initTranslations();
106
+ expect(navigator.language).toBe('de-DE');
107
+ expect(I18nService.currentLanguage).toBe('en');
108
+ expect(I18nService.translate('home.title')).toBe('My App');
109
+ I18nService.resetTranslations();
110
+
111
+ // missing translation
112
+ Object.defineProperty(navigator, 'language', {
113
+ writable: true,
114
+ configurable: true,
115
+ value: "fr-FR"
116
+ });
117
+ await I18nService.initTranslations();
118
+ expect(navigator.language).toBe('fr-FR');
119
+ expect(I18nService.currentLanguage).toBe('fr');
120
+ expect(I18nService.translate('home.title')).toBe('Mon App');
121
+ expect(I18nService.translate('home.subtitle')).toBe('English subtitle');
122
+ });
123
+
124
+ it('should return key when translation is missing', () => {
125
+ expect(I18nService.translate('non.existent.key')).toBe('non.existent.key');
126
+ });
127
+
128
+ it('setCurrentLanguage updates the language, persists it and loads its translations', async () => {
129
+ await I18nService.setCurrentLanguage('fr');
130
+
131
+ expect(I18nService.currentLanguage).toBe('fr');
132
+ expect(I18nService.translate('home.title')).toBe('Mon App');
133
+ expect(localStorage.getItem(I18nService.localStorageKey)).toBe('fr');
134
+ });
135
+
136
+ it('setCurrentLanguage emits a language-changed event with the new language', async () => {
137
+ const received = [];
138
+ const callback = (data) => received.push(data);
139
+ I18nService.onLanguageChange(callback);
140
+
141
+ await I18nService.setCurrentLanguage('fr');
142
+
143
+ expect(received).toEqual([{ lang: 'fr' }]);
144
+ AppEventBus.off('language-changed', callback);
145
+ });
146
+
147
+ it('getTranslationObject returns the nested value for the current language', async () => {
148
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
149
+ await I18nService.initTranslations();
150
+ expect(I18nService.getTranslationObject('home')).toEqual({ title: 'My App', subtitle: 'English subtitle' });
151
+ });
152
+
153
+ it('getTranslationObject falls back to the default language when missing in the current language', async () => {
154
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'fr-FR' });
155
+ await I18nService.initTranslations();
156
+ expect(I18nService.getTranslationObject('home.subtitle')).toBe('English subtitle');
157
+ });
158
+
159
+ it('getTranslationObject returns null when the key is not found anywhere', async () => {
160
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
161
+ await I18nService.initTranslations();
162
+ expect(I18nService.getTranslationObject('does.not.exist')).toBe(null);
163
+ });
164
+
165
+ it('honors a locale prefix in the URL path when CONFIG.i18n is enabled', async () => {
166
+ CONFIG.i18n = true;
167
+ window.history.pushState({}, '', '/fr/some/page');
168
+ try {
169
+ await I18nService.initTranslations();
170
+ expect(I18nService.currentLanguage).toBe('fr');
171
+ } finally {
172
+ CONFIG.i18n = false;
173
+ window.history.pushState({}, '', '/');
174
+ }
175
+ });
176
+
177
+ it('ignores a locale prefix in the URL path when CONFIG.i18n is disabled (default)', async () => {
178
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
179
+ window.history.pushState({}, '', '/fr/some/page');
180
+ try {
181
+ await I18nService.initTranslations();
182
+ expect(I18nService.currentLanguage).toBe('en');
183
+ } finally {
184
+ window.history.pushState({}, '', '/');
185
+ }
186
+ });
187
+
188
+ it('initTranslations falls back to the default language when the resolved preferred language is unsupported', async () => {
189
+ const original = I18nService.getPreferredLanguage;
190
+ I18nService.getPreferredLanguage = () => 'zz';
191
+ try {
192
+ await I18nService.initTranslations();
193
+ expect(I18nService.currentLanguage).toBe('en');
194
+ } finally {
195
+ I18nService.getPreferredLanguage = original;
196
+ }
197
+ });
198
+
199
+ it('getLocaleFromUrl returns null when the URL locale segment is not supported', () => {
200
+ CONFIG.i18n = true;
201
+ window.history.pushState({}, '', '/xx/page');
202
+ try {
203
+ expect(I18nService.getLocaleFromUrl()).toBe(null);
204
+ } finally {
205
+ CONFIG.i18n = false;
206
+ window.history.pushState({}, '', '/');
207
+ }
208
+ });
209
+
210
+ it('loadDefaultTranslations logs and leaves default translations empty when the fetch fails', async () => {
211
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'fr-FR' });
212
+ const errSpy = spyOn(console, 'error');
213
+ errSpy.andCallFake(() => {});
214
+ fetchMock.mockError('/locales/en.json', 500);
215
+
216
+ await I18nService.initTranslations();
217
+
218
+ // loadDefaultTranslations is attempted twice: once explicitly by initTranslations,
219
+ // and again from within loadTranslations('fr') since _defaultTranslations is still
220
+ // empty after the first failure — each attempt logs its own error.
221
+ expect(errSpy.callCount).toBe(2);
222
+ expect(I18nService.currentLanguage).toBe('fr');
223
+
224
+ errSpy.restore();
225
+ });
226
+
227
+ it('honors a previously saved language preference from localStorage', async () => {
228
+ localStorage.setItem(I18nService.localStorageKey, 'fr');
229
+ await I18nService.initTranslations();
230
+ expect(I18nService.currentLanguage).toBe('fr');
231
+ });
232
+
233
+ it('loadTranslations logs and leaves the language unchanged when the fetch rejects', async () => {
234
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
235
+ const errSpy = spyOn(console, 'error');
236
+ errSpy.andCallFake(() => {});
237
+ global.fetch = () => Promise.reject(new Error('network down'));
238
+
239
+ await I18nService.initTranslations();
240
+
241
+ expect(errSpy.callCount).toBe(1);
242
+ expect(I18nService.currentLanguage).toBe('en');
243
+
244
+ errSpy.restore();
245
+ });
246
+
247
+ it('loadTranslations logs and leaves state unchanged when the response is not ok', async () => {
248
+ Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
249
+ const errSpy = spyOn(console, 'error');
250
+ errSpy.andCallFake(() => {});
251
+ fetchMock.mockError('/locales/en.json', 500);
252
+
253
+ await I18nService.initTranslations();
254
+
255
+ expect(errSpy.callCount).toBe(1);
256
+ expect(I18nService.currentLanguage).toBe('en');
257
+
258
+ errSpy.restore();
259
+ });
260
+
261
+ it('onDestroy resets translations and disposes the instance', async () => {
262
+ await I18nService.initTranslations();
263
+ expect(I18nService.translate('home.title')).toBe('My App');
264
+
265
+ I18nService.onDestroy();
266
+
267
+ expect(I18nService.translate('home.title')).toBe('home.title');
268
+ expect(localStorage.getItem(I18nService.localStorageKey)).toBe(null);
269
+ });
270
+ });
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_LANGUAGE, LANGUAGES, Language, Translations } from './languages.js';
2
2
  import { AppEventBus, Service } from '../../index.js';
3
+ import { CONFIG } from '../../config.js';
3
4
 
4
5
  export class I18n extends Service<I18n> {
5
6
  private readonly _DEFAULT_LANGUAGE: Language = DEFAULT_LANGUAGE;
@@ -35,7 +36,11 @@ export class I18n extends Service<I18n> {
35
36
  this._currentLanguage = lang;
36
37
  this.savePreferences();
37
38
  await this.loadTranslations(lang);
38
- AppEventBus.emit('language-changed');
39
+ AppEventBus.emit('language-changed', { lang });
40
+ }
41
+
42
+ public onLanguageChange(callback: (payload: { lang: string }) => void) {
43
+ AppEventBus.subscribe('language-changed', callback);
39
44
  }
40
45
 
41
46
  public async loadTranslations(lang: Language): Promise<void> {
@@ -99,6 +104,7 @@ export class I18n extends Service<I18n> {
99
104
  public resetTranslations(): void {
100
105
  this._translations = {};
101
106
  this._defaultTranslations = {};
107
+ localStorage.removeItem(this._localStorageKey);
102
108
  this._currentLanguage = this._DEFAULT_LANGUAGE;
103
109
  }
104
110
 
@@ -107,9 +113,21 @@ export class I18n extends Service<I18n> {
107
113
  this.dispose();
108
114
  }
109
115
 
110
- private getPreferredLanguage() {
111
- return this.getPreferences() || navigator.language.split('-')[0] as Language;
112
- };
116
+ private getPreferredLanguage(): Language {
117
+ const urlLocale = this.getLocaleFromUrl();
118
+ if (urlLocale) return urlLocale;
119
+ const stored = this.getPreferences();
120
+ if (stored && this._LANGUAGES.includes(stored as Language)) return stored as Language;
121
+ const browserLang = navigator.language.split('-')[0] as Language;
122
+ return this._LANGUAGES.includes(browserLang) ? browserLang : this._DEFAULT_LANGUAGE;
123
+ }
124
+
125
+ private getLocaleFromUrl(): Language | null {
126
+ if (!CONFIG.i18n) return null;
127
+
128
+ const first = window.location.pathname.split('/').filter(Boolean)[0] as Language;
129
+ return first && this._LANGUAGES.includes(first) ? first : null;
130
+ }
113
131
 
114
132
  private async loadDefaultTranslations(): Promise<void> {
115
133
  try {
@@ -1,8 +1,7 @@
1
- export * from './event-bus/event-bus.js';
1
+ export * from './event-bus/index.js';
2
+ export * from './globals/globals.js';
2
3
  export * from './http-client/http-client.js';
3
- export * from './router/router.js';
4
- export * from './store/store.js';
5
- export * from './pipe-registry/pipe-registry.js';
6
- {{#if i18n}}
7
4
  export * from './i18n/i18n.js';
8
- {{/if}}
5
+ export * from './pipe-registry/pipe-registry.js';
6
+ export * from './pipe-registry/pipes.js';
7
+ export * from './router/router.js';
@@ -1,7 +1,66 @@
1
1
  import { AppPipeRegistry } from '#root/dist/src/core/services/index.js';
2
+ import { registerPipes } from '#root/dist/src/core/services/pipe-registry/pipes.js';
2
3
 
3
4
  describe('PipeRegistry', () => {
4
5
  it('should be defined', () => {
5
6
  expect(AppPipeRegistry).toBeDefined();
6
- })
7
+ });
8
+
9
+ it('register() makes a pipe available via apply()', () => {
10
+ AppPipeRegistry.register('reverse-test', (value) => String(value).split('').reverse().join(''));
11
+ expect(AppPipeRegistry.apply('reverse-test', 'abc')).toBe('cba');
12
+ });
13
+
14
+ it('apply() passes extra args through to the pipe function', () => {
15
+ AppPipeRegistry.register('pad-test', (value, char) => `${char}${value}${char}`);
16
+ expect(AppPipeRegistry.apply('pad-test', 'x', ['*'])).toBe('*x*');
17
+ });
18
+
19
+ it('register() keeps the original implementation when the name is already registered', () => {
20
+ const warnSpy = spyOn(console, 'warn');
21
+ warnSpy.andCallFake(() => {});
22
+
23
+ AppPipeRegistry.register('dup-test', () => 'first');
24
+
25
+ AppPipeRegistry.register('dup-test', () => 'second');
26
+
27
+ expect(AppPipeRegistry.apply('dup-test', 'ignored')).toBe('first');
28
+
29
+ expect(warnSpy.callCount).toBe(1);
30
+ warnSpy.restore();
31
+ });
32
+
33
+ it('apply() warns and returns the raw value when the pipe is not registered', () => {
34
+ const warnSpy = spyOn(console, 'warn');
35
+ warnSpy.andCallFake(() => {});
36
+
37
+ const result = AppPipeRegistry.apply('does-not-exist', 'raw-value');
38
+
39
+ expect(result).toBe('raw-value');
40
+ expect(warnSpy.callCount).toBe(1);
41
+
42
+ warnSpy.restore();
43
+ });
44
+
45
+ it('onDestroy() clears all registered pipes', () => {
46
+ AppPipeRegistry.register('clear-test', (v) => v);
47
+ expect(AppPipeRegistry.apply('clear-test', 'x')).toBe('x');
48
+
49
+ AppPipeRegistry.onDestroy();
50
+
51
+ const warnSpy = spyOn(console, 'warn');
52
+ warnSpy.andCallFake(() => {});
53
+
54
+ expect(AppPipeRegistry.apply('clear-test', 'x')).toBe('x');
55
+ expect(warnSpy.callCount).toBe(1);
56
+
57
+ warnSpy.restore();
58
+
59
+ // AppPipeRegistry is a process-wide singleton shared by every test suite,
60
+ // so destroying it here would otherwise leak into every suite that runs
61
+ // afterward and relies on the app's pipes already being registered. Silenced
62
+ // since this can legitimately re-hit an "already exists" warning per pipe if
63
+ // some other suite re-registers them again later in the run.
64
+ silenceConsole('warn', () => registerPipes());
65
+ });
7
66
  });
@@ -9,7 +9,7 @@ class PipeRegistry extends Service<PipeRegistry> {
9
9
 
10
10
  public register(name: string, fn: PipeFunction): void {
11
11
  if(this._pipes[name]) {
12
- console.warn(`Skipping pipe "${name}": A pipe with this name already exists.`);
12
+ console.warn(`Pipe "${name}" already exists - skipping.`);
13
13
  return;
14
14
  }
15
15
  this._pipes[name] = fn;
@@ -0,0 +1,128 @@
1
+ import { registerPipes } from '#root/dist/src/core/services/pipe-registry/pipes.js';
2
+ import { AppPipeRegistry } from '#root/dist/src/core/index.js';
3
+
4
+ describe('pipes (registerPipes)', () => {
5
+ beforeAll(() => {
6
+ // AppPipeRegistry is a process-wide singleton shared by every suite in the
7
+ // run - if an earlier suite (e.g. pipe-registry.test.js) already re-registered
8
+ // the app's pipes, this call is a legitimate no-op that logs a "already exists"
9
+ // warning per pipe. Silence that expected noise rather than letting it leak.
10
+ silenceConsole('warn', () => registerPipes());
11
+ });
12
+
13
+ it('currency formats a number as USD/en-US by default', () => {
14
+ const expected = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5);
15
+ expect(AppPipeRegistry.apply('currency', 1234.5)).toBe(expected);
16
+ });
17
+
18
+ it('currency accepts a custom currency and locale via args', () => {
19
+ const expected = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1000);
20
+ expect(AppPipeRegistry.apply('currency', 1000, ['EUR', 'de-DE'])).toBe(expected);
21
+ });
22
+
23
+ it('date formats using the "long" format by default', () => {
24
+ const value = '2024-03-15T00:00:00Z';
25
+ const expected = new Date(value).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
26
+ expect(AppPipeRegistry.apply('date', value, ['en-US'])).toBe(expected);
27
+ });
28
+
29
+ it('date formats using the "short" format when requested', () => {
30
+ const value = '2024-03-15T00:00:00Z';
31
+ const expected = new Date(value).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
32
+ expect(AppPipeRegistry.apply('date', value, ['en-US', 'short'])).toBe(expected);
33
+ });
34
+
35
+ it('date formats using the "time" format when requested', () => {
36
+ const value = '2024-03-15T14:30:00Z';
37
+ const expected = new Date(value).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit' });
38
+ expect(AppPipeRegistry.apply('date', value, ['en-US', 'time'])).toBe(expected);
39
+ });
40
+
41
+ it('date falls back to the "long" format for an unrecognized format name', () => {
42
+ const value = '2024-03-15T00:00:00Z';
43
+ const expected = new Date(value).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
44
+ expect(AppPipeRegistry.apply('date', value, ['en-US', 'nonsense'])).toBe(expected);
45
+ });
46
+
47
+ it('date includes hour/minute in short/long formats when the time flag is set', () => {
48
+ const value = '2024-03-15T14:30:00Z';
49
+ const expected = new Date(value).toLocaleString('en-US', {
50
+ year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
51
+ });
52
+ expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', true])).toBe(expected);
53
+ });
54
+
55
+ it('date warns and returns the raw stringified value for an invalid date', () => {
56
+ const warnSpy = spyOn(console, 'warn');
57
+ warnSpy.andCallFake(() => {});
58
+
59
+ const result = AppPipeRegistry.apply('date', 'not-a-date', ['en-US']);
60
+
61
+ expect(result).toBe('not-a-date');
62
+ expect(warnSpy.callCount).toBe(1);
63
+ warnSpy.restore();
64
+ });
65
+
66
+ it('number defaults to 0 decimal places', () => {
67
+ expect(AppPipeRegistry.apply('number', 3.14159)).toBe('3');
68
+ });
69
+
70
+ it('number respects an explicit decimals arg', () => {
71
+ expect(AppPipeRegistry.apply('number', 3.14159, ['2'])).toBe('3.14');
72
+ });
73
+
74
+ it('uppercase converts to upper case', () => {
75
+ expect(AppPipeRegistry.apply('uppercase', 'hello')).toBe('HELLO');
76
+ });
77
+
78
+ it('lowercase converts to lower case', () => {
79
+ expect(AppPipeRegistry.apply('lowercase', 'HELLO')).toBe('hello');
80
+ });
81
+
82
+ it('capitalize uppercases only the first letter and lowercases the rest', () => {
83
+ expect(AppPipeRegistry.apply('capitalize', 'HELLO world')).toBe('Hello world');
84
+ });
85
+
86
+ it('capitalizeAll capitalizes the first letter of every word', () => {
87
+ expect(AppPipeRegistry.apply('capitalizeAll', 'hello world')).toBe('Hello World');
88
+ });
89
+
90
+ it('capitalizeAll capitalizes after separators like comma and period', () => {
91
+ expect(AppPipeRegistry.apply('capitalizeAll', 'hello, world. goodbye')).toBe('Hello, World. Goodbye');
92
+ });
93
+
94
+ it('capitalizeAll does not capitalize a letter immediately following an apostrophe', () => {
95
+ expect(AppPipeRegistry.apply('capitalizeAll', "o'brien's house")).toBe("O'brien's House");
96
+ });
97
+
98
+ it('truncate leaves a string shorter than the given length unchanged', () => {
99
+ expect(AppPipeRegistry.apply('truncate', 'Hi', ['5'])).toBe('Hi');
100
+ });
101
+
102
+ it('truncate cuts a longer string and appends the default suffix', () => {
103
+ expect(AppPipeRegistry.apply('truncate', 'Hello World', ['5'])).toBe('Hello...');
104
+ });
105
+
106
+ it('truncate accepts a custom suffix', () => {
107
+ expect(AppPipeRegistry.apply('truncate', 'Hello World', ['5', '---'])).toBe('Hello---');
108
+ });
109
+
110
+ it('default returns the value unchanged when it is truthy', () => {
111
+ expect(AppPipeRegistry.apply('default', 'value', ['N/A'])).toBe('value');
112
+ });
113
+
114
+ it('default falls back to the given default when the value is falsy', () => {
115
+ expect(AppPipeRegistry.apply('default', '', ['N/A'])).toBe('N/A');
116
+ expect(AppPipeRegistry.apply('default', 0, ['N/A'])).toBe('N/A');
117
+ });
118
+
119
+ it('json pretty-prints a plain value', () => {
120
+ expect(AppPipeRegistry.apply('json', { a: 1 })).toBe(JSON.stringify({ a: 1 }, null, 2));
121
+ });
122
+
123
+ it('json falls back to String(value) when JSON.stringify throws (e.g. a circular reference)', () => {
124
+ const circular = {};
125
+ circular.self = circular;
126
+ expect(AppPipeRegistry.apply('json', circular)).toBe(String(circular));
127
+ });
128
+ });
@@ -1,4 +1,4 @@
1
- import { AppPipeRegistry } from "../../core/index.js";
1
+ import { AppPipeRegistry } from "./pipe-registry.js";
2
2
 
3
3
  export const registerPipes = (): void => {
4
4
  // currency
@@ -21,9 +21,12 @@ export const registerPipes = (): void => {
21
21
  time = false,
22
22
  ) => {
23
23
  const date = new Date(value);
24
+ // '' is not a valid Intl.DateTimeFormatOptions value for hour/minute, so
25
+ // omit those keys entirely rather than setting them when time is false.
26
+ const timeOptions: Intl.DateTimeFormatOptions = time ? { hour: '2-digit', minute: '2-digit' } : {};
24
27
  const formats = {
25
- short: { year: 'numeric', month: 'short', day: 'numeric', hour: time ? '2-digit' : '', minute: time ? '2-digit' : '' } as Intl.DateTimeFormatOptions,
26
- long: { year: 'numeric', month: 'long', day: 'numeric', hour: time ? '2-digit' : '', minute: time ? '2-digit' : '' } as Intl.DateTimeFormatOptions,
28
+ short: { year: 'numeric', month: 'short', day: 'numeric', ...timeOptions } as Intl.DateTimeFormatOptions,
29
+ long: { year: 'numeric', month: 'long', day: 'numeric', ...timeOptions } as Intl.DateTimeFormatOptions,
27
30
  time: { hour: '2-digit', minute: '2-digit' } as Intl.DateTimeFormatOptions
28
31
  };
29
32
 
@@ -51,10 +54,10 @@ export const registerPipes = (): void => {
51
54
  });
52
55
  AppPipeRegistry.register('capitalizeAll', (value) => {
53
56
  const str = String(value);
54
- return str.replace(/(^|[ .,"'])\s*([a-z])/g, (match, separator, char, offset) => {
57
+ return str.replace(/(^|[ .,"'])(\s*)([a-z])/g, (match, separator, whitespace, char, offset) => {
55
58
  // Don't capitalize if preceded by a single quote (apostrophe)
56
59
  if (separator === "'" && offset > 0) return match;
57
- return separator + char.toUpperCase();
60
+ return separator + whitespace + char.toUpperCase();
58
61
  });
59
62
  });
60
63
  AppPipeRegistry.register('truncate', (value, length = 50, suffix = '...') => {