@nutin/cli 1.3.0 → 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 (284) hide show
  1. package/CHANGELOG.md +122 -8
  2. package/README.md +21 -37
  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 +81 -30
  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/create-app/new-app-prompt.mjs +51 -0
  22. package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +15 -13
  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 -15
  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 +81 -31
  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 +38 -23
  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 +41 -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 +13 -14
  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 +27 -40
  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/config.helper.ts.hbs +17 -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 +74 -0
  74. package/templates/base/src/core/base-classes/view/view.ts.hbs +17 -41
  75. package/templates/base/src/core/config.ts.hbs +7 -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 +149 -0
  87. package/templates/base/src/core/services/globals/globals.ts.hbs +95 -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 +43 -0
  91. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  92. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +60 -9
  93. package/templates/base/src/core/services/index.ts.hbs +5 -6
  94. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  95. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  96. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  97. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  98. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +149 -6
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +42 -30
  100. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  101. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  102. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +13 -70
  103. package/templates/base/src/core/services/router/router.test.js.hbs +226 -2
  104. package/templates/base/src/core/services/router/router.ts.hbs +40 -9
  105. package/templates/base/src/index.html.hbs +2 -6
  106. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  107. package/templates/base/src/styles/base/_variables.scss.hbs +1 -85
  108. package/templates/base/src/styles/main.scss.hbs +1 -18
  109. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  110. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  111. package/templates/base/tools/builder/builder.js.hbs +24 -26
  112. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  113. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  114. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  115. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  116. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  117. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  118. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  119. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  120. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  121. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  122. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  123. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  124. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  125. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  126. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  127. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  128. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  129. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  130. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  131. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  132. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  133. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  134. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  136. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  137. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  138. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  139. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  140. package/templates/base/tools/dev/serve.js.hbs +11 -1
  141. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  142. package/templates/base/tools/generator/generator.js.hbs +64 -47
  143. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  144. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  145. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  146. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  147. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  148. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  149. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  150. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +4 -0
  151. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +7 -0
  152. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  153. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  154. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  155. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  156. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  157. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  158. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +3 -2
  159. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  160. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  164. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  165. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  166. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  167. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  168. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  169. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  170. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  171. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  172. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  173. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  174. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  175. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  176. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  177. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  178. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  179. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  180. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  181. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  182. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  183. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  185. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  186. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  187. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  188. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  189. package/templates/base/tools/utils/format.js.hbs +10 -10
  190. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  191. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  192. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  193. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  194. package/templates/base/tools/utils/index.js.hbs +1 -3
  195. package/templates/base/tools/utils/languages.js.hbs +15 -0
  196. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  197. package/templates/base/tools/utils/print.js.hbs +19 -13
  198. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  199. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  200. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  201. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  202. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  203. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  204. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  205. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  206. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  207. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  208. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  209. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  211. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  212. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  213. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  214. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  215. package/lib/src/context-builder.mjs +0 -35
  216. package/lib/src/file-generator.mjs +0 -109
  217. package/lib/src/json-generator.mjs +0 -31
  218. package/lib/src/package-manager.mjs +0 -139
  219. package/lib/src/print.mjs +0 -31
  220. package/lib/src/prompts.mjs +0 -123
  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 -35
  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 -85
  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 -108
  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/popover.ts.hbs +0 -145
  239. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -72
  240. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  241. package/templates/base/src/styles/core/_libs.scss.hbs +0 -132
  242. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  243. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  244. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  245. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  246. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  247. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  248. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  249. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  250. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  251. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  252. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  253. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  254. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  255. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  256. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  257. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  258. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  259. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  260. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -240
  261. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  262. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  263. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  264. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  265. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  266. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  267. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  268. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  269. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  270. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  271. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  272. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  273. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  274. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  275. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  276. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  277. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  278. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  279. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  280. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  281. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  282. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  283. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  284. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -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
- import { Service } from '../../index.js';
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;
@@ -7,11 +8,12 @@ export class I18n extends Service<I18n> {
7
8
  private _currentLanguage: Language = this._DEFAULT_LANGUAGE;
8
9
  private _translations: Translations = {};
9
10
  private _defaultTranslations: Translations = {};
11
+ private readonly _localStorageKey = 'nutin-fav-lang';
10
12
 
11
13
  constructor() {
12
14
  super();
13
15
  this.registerCleanup(this.resetTranslations);
14
- this._currentLanguage = this.getBrowserLanguage();
16
+ this._currentLanguage = this.getPreferredLanguage();
15
17
  }
16
18
 
17
19
  public get currentLanguage(): Language {
@@ -26,6 +28,21 @@ export class I18n extends Service<I18n> {
26
28
  return this._LANGUAGES;
27
29
  }
28
30
 
31
+ public get localStorageKey(): string {
32
+ return this._localStorageKey;
33
+ }
34
+
35
+ public async setCurrentLanguage(lang: Language): Promise<void> {
36
+ this._currentLanguage = lang;
37
+ this.savePreferences();
38
+ await this.loadTranslations(lang);
39
+ AppEventBus.emit('language-changed', { lang });
40
+ }
41
+
42
+ public onLanguageChange(callback: (payload: { lang: string }) => void) {
43
+ AppEventBus.subscribe('language-changed', callback);
44
+ }
45
+
29
46
  public async loadTranslations(lang: Language): Promise<void> {
30
47
  try {
31
48
  const response = await fetch(`/locales/${lang}.json`);
@@ -39,10 +56,11 @@ export class I18n extends Service<I18n> {
39
56
  await this.loadDefaultTranslations();
40
57
  }
41
58
 
42
- document.documentElement.lang = lang;
59
+ document.documentElement.lang = lang;
60
+ this.savePreferences();
43
61
  } catch (error) {
44
62
  console.error('Translation load error:', error);
45
- }
63
+ }
46
64
  }
47
65
 
48
66
  public translate(key: string, textContent?: string | null): string {
@@ -60,8 +78,8 @@ export class I18n extends Service<I18n> {
60
78
  }
61
79
 
62
80
  public async initTranslations(): Promise<void> {
63
- const browserLang = this.getBrowserLanguage();
64
- const lang = this._LANGUAGES.includes(browserLang) ? browserLang : this._DEFAULT_LANGUAGE;
81
+ const preferredLang = this.getPreferredLanguage();
82
+ const lang = this._LANGUAGES.includes(preferredLang) ? preferredLang : this._DEFAULT_LANGUAGE;
65
83
 
66
84
  // Always load default translations first
67
85
  if (lang !== this._DEFAULT_LANGUAGE) {
@@ -71,9 +89,22 @@ export class I18n extends Service<I18n> {
71
89
  await this.loadTranslations(lang);
72
90
  }
73
91
 
92
+ public getTranslationObject<T = any>(key: string): T | null {
93
+ const keys = key.split('.');
94
+
95
+ let value = this.getNestedValue(this._translations, keys);
96
+
97
+ if (!value && this._currentLanguage !== this._DEFAULT_LANGUAGE) {
98
+ value = this.getNestedValue(this._defaultTranslations, keys);
99
+ }
100
+
101
+ return value || null;
102
+ }
103
+
74
104
  public resetTranslations(): void {
75
105
  this._translations = {};
76
106
  this._defaultTranslations = {};
107
+ localStorage.removeItem(this._localStorageKey);
77
108
  this._currentLanguage = this._DEFAULT_LANGUAGE;
78
109
  }
79
110
 
@@ -82,9 +113,21 @@ export class I18n extends Service<I18n> {
82
113
  this.dispose();
83
114
  }
84
115
 
85
- private getBrowserLanguage() {
86
- return navigator.language.split('-')[0] as Language;
87
- };
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
+ }
88
131
 
89
132
  private async loadDefaultTranslations(): Promise<void> {
90
133
  try {
@@ -106,6 +149,14 @@ export class I18n extends Service<I18n> {
106
149
  }
107
150
  return value;
108
151
  }
152
+
153
+ private savePreferences(): void {
154
+ localStorage.setItem(this._localStorageKey, this._currentLanguage);
155
+ }
156
+
157
+ private getPreferences(): string | null {
158
+ return localStorage.getItem(this._localStorageKey);
159
+ }
109
160
  }
110
161
 
111
162
  export const I18nService = I18n.getInstance();
@@ -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 = '...') => {