@nutin/cli 2.1.0 → 3.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 (241) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/README.md +14 -6
  3. package/bin/add-feature.mjs +1 -1
  4. package/bin/cli.mjs +1 -1
  5. package/bin/create-app.mjs +1 -1
  6. package/bin/update-app.mjs +1 -1
  7. package/lib/index.mjs +6 -2
  8. package/lib/src/add-feature/base-template-patcher.mjs +139 -0
  9. package/lib/src/add-feature/feature-adder.mjs +20 -9
  10. package/lib/src/add-feature/feature-context-builder.mjs +1 -0
  11. package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
  12. package/lib/src/add-feature/package-json-updater.mjs +7 -9
  13. package/lib/src/common/feature-registry.mjs +4 -0
  14. package/lib/src/common/file-generator.mjs +25 -3
  15. package/lib/src/common/git.mjs +31 -0
  16. package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
  17. package/lib/src/common/package-json-helper.mjs +14 -4
  18. package/lib/src/common/project-meta.mjs +2 -2
  19. package/lib/src/common/template-compiler.mjs +2 -0
  20. package/lib/src/common/transpile-ts.mjs +94 -0
  21. package/lib/src/create-app/context-builder.mjs +1 -0
  22. package/lib/src/create-app/git-manager.mjs +12 -2
  23. package/lib/src/create-app/json-manager.mjs +21 -18
  24. package/lib/src/create-app/new-app-prompt.mjs +4 -2
  25. package/lib/src/create-app/success-message.mjs +1 -1
  26. package/lib/src/update-app/conflict-report-writer.mjs +33 -21
  27. package/lib/src/update-app/json-drift.mjs +54 -0
  28. package/lib/src/update-app/project-updater.mjs +123 -14
  29. package/lib/src/update-app/template-differ.mjs +5 -5
  30. package/lib/src/update-app/three-way-merge.mjs +42 -0
  31. package/lib/src/update-app/update-context-builder.mjs +1 -0
  32. package/lib/src/update-app/update-summary-printer.mjs +10 -2
  33. package/package.json +12 -11
  34. package/templates/base/.gitignore.hbs +1 -0
  35. package/templates/base/AGENTS.md.hbs +45 -43
  36. package/templates/base/GETTING_STARTED.md.hbs +32 -23
  37. package/templates/base/README.md.hbs +7 -4
  38. package/templates/base/nutin.config.js.hbs +1 -1
  39. package/templates/base/src/app/components/.gitkeep +0 -0
  40. package/templates/base/src/app/globals.d.ts.hbs +6 -3
  41. package/templates/base/src/app/guards.ts.hbs +1 -3
  42. package/templates/base/src/app/main.ts.hbs +2 -9
  43. package/templates/base/src/app/routes.ts.hbs +9 -3
  44. package/templates/base/src/app/services/.gitkeep +0 -0
  45. package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
  46. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  47. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
  48. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
  49. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
  50. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
  51. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
  52. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +307 -21
  53. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
  54. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +105 -4
  55. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +136 -62
  56. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
  57. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +101 -51
  58. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
  59. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
  60. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
  61. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
  62. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
  63. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
  64. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
  65. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
  66. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
  67. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
  68. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
  69. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
  70. package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
  71. package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
  72. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
  73. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
  74. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
  75. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
  76. package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
  77. package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
  78. package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
  79. package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
  80. package/templates/base/src/core/index.ts.hbs +4 -2
  81. package/templates/base/src/core/internals.d.ts.hbs +199 -20
  82. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
  83. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
  84. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
  85. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
  86. package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
  87. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
  88. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
  89. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
  90. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
  91. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
  92. package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
  93. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
  94. package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
  95. package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
  96. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
  97. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
  98. package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +217 -4
  100. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +210 -65
  101. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
  102. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
  103. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
  104. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
  105. package/templates/base/src/core/services/router/router.test.js.hbs +321 -0
  106. package/templates/base/src/core/services/router/router.ts.hbs +122 -54
  107. package/templates/base/src/index.html.hbs +1 -0
  108. package/templates/base/src/styles/_styles.scss.hbs +1 -113
  109. package/templates/base/src/styles/main.scss.hbs +1 -1
  110. package/templates/base/tools/builder/builder.config.js.hbs +6 -0
  111. package/templates/base/tools/builder/builder.js.hbs +26 -8
  112. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
  113. package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
  114. package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
  115. package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
  116. package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
  117. package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
  118. package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
  119. package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
  120. package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
  121. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
  122. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
  123. package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
  124. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
  125. package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
  126. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
  127. package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
  128. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
  129. package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
  130. package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
  131. package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
  132. package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
  133. package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
  134. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
  135. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +25 -8
  136. package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
  137. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
  138. package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
  139. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
  140. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +61 -15
  141. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
  142. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
  143. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +48 -18
  144. package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
  145. package/templates/base/tools/builder/core/seo/route-output.js.hbs +26 -8
  146. package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
  147. package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
  148. package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
  149. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
  150. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
  151. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
  152. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +26 -9
  153. package/templates/base/tools/builder/core/styles/sass.js.hbs +7 -5
  154. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
  155. package/templates/base/tools/dev/dev-serve.js.hbs +71 -7
  156. package/templates/base/tools/dev/serve.js.hbs +152 -22
  157. package/templates/base/tools/dev/watcher.js.hbs +75 -28
  158. package/templates/base/tools/generator/generator.js.hbs +23 -18
  159. package/templates/base/tools/generator/handle-file.js.hbs +4 -24
  160. package/templates/base/tools/generator/handle-file.test.js.hbs +4 -23
  161. package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
  162. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
  163. package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
  164. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
  165. package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
  166. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  167. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
  168. package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
  169. package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
  170. package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
  171. package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
  172. package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
  173. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
  174. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
  175. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
  176. package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
  177. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
  178. package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
  179. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
  180. package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
  181. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
  182. package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
  183. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
  184. package/templates/base/tools/utils/build-lock.js.hbs +97 -0
  185. package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
  186. package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
  187. package/templates/base/tools/utils/format.js.hbs +1 -1
  188. package/templates/base/tools/utils/index.js.hbs +3 -1
  189. package/templates/base/tools/utils/print.js.hbs +2 -2
  190. package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
  191. package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
  192. package/templates/base/tools/utils/run-command.js.hbs +30 -1
  193. package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
  194. package/templates/base/tools/utils/run-script.js.hbs +2 -2
  195. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +5 -2
  196. package/templates/features/docker/tools/docker/nginx.conf.template +19 -2
  197. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
  198. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
  199. package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
  200. package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
  201. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
  202. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
  203. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
  204. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
  205. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +29 -0
  206. package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
  207. package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
  208. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
  209. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
  210. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
  211. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
  212. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
  213. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
  214. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
  215. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
  216. package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
  217. package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
  218. package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
  219. package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
  220. package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
  221. package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
  222. package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
  223. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
  224. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
  225. package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
  226. package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
  227. package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
  228. package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
  229. package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
  230. package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
  231. package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
  232. package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
  233. package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
  234. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
  235. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
  236. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
  237. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
  238. package/templates/base/src/app/components/index.ts.hbs +0 -1
  239. package/templates/base/src/app/services/index.ts.hbs +0 -1
  240. package/templates/base/src/app/views/index.ts.hbs +0 -3
  241. /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
@@ -1,35 +1,42 @@
1
- import { Service } from "../../index.js";
2
- import { HttpBuilder, HttpMethod, IRequestConfig } from "./helpers/http-builder.helper.js";
3
- import { HttpManager } from "./helpers/http-manager.helper.js";
1
+ import { Service } from "../../base-classes/service/service.js";
2
+ import * as HttpBuilder from "./helpers/http-builder.helper.js";
3
+ import * as HttpManager from "./helpers/http-manager.helper.js";
4
4
 
5
- export type IHttpClient = InstanceType<typeof HttpClient>;
5
+ export { path } from "./helpers/http-builder.helper.js";
6
+ export { HttpError } from "./helpers/http-manager.helper.js";
6
7
 
7
8
  export class HttpClient extends Service<HttpClient> {
8
9
  private _baseUrl: string;
9
10
  private _defaultHeaders: Record<string, string>;
11
+ private _trustedAPIs: string[];
10
12
 
11
13
  private _requestInterceptors: Array<(url: string, options: RequestInit) => void> = [];
12
14
  private _responseInterceptors: Array<(response: Response) => void> = [];
13
15
 
14
- constructor(baseUrl: string = '', defaultHeaders: Record<string, string> = {}) {
16
+ constructor(
17
+ baseUrl: string = '',
18
+ defaultHeaders: Record<string, string> = {},
19
+ options: Nutin.HttpClientOptions = {}
20
+ ) {
15
21
  super();
16
22
  this._baseUrl = baseUrl;
17
- this._defaultHeaders = { 'Content-Type': 'application/json', ...defaultHeaders };
23
+ this._defaultHeaders = { ...defaultHeaders };
24
+ this._trustedAPIs = [...(options.trustedAPIs ?? [])];
18
25
  }
19
26
 
20
- public get<T = unknown>(endpoint: string, config?: IRequestConfig): Promise<T> {
27
+ public get<T = unknown>(endpoint: string, config?: Nutin.RequestConfig): Promise<T> {
21
28
  return this.request<T>('GET', endpoint, null, config);
22
29
  }
23
- public post<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
30
+ public post<T = unknown>(endpoint: string, data?: unknown, config?: Nutin.RequestConfig): Promise<T> {
24
31
  return this.request<T>('POST', endpoint, data, config);
25
32
  }
26
- public put<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
33
+ public put<T = unknown>(endpoint: string, data?: unknown, config?: Nutin.RequestConfig): Promise<T> {
27
34
  return this.request<T>('PUT', endpoint, data, config);
28
35
  }
29
- public patch<T = unknown>(endpoint: string, data?: unknown, config?: IRequestConfig): Promise<T> {
36
+ public patch<T = unknown>(endpoint: string, data?: unknown, config?: Nutin.RequestConfig): Promise<T> {
30
37
  return this.request<T>('PATCH', endpoint, data, config);
31
38
  }
32
- public delete<T = unknown>(endpoint: string, config?: IRequestConfig): Promise<T> {
39
+ public delete<T = unknown>(endpoint: string, config?: Nutin.RequestConfig): Promise<T> {
33
40
  return this.request<T>('DELETE', endpoint, undefined, config);
34
41
  }
35
42
 
@@ -37,6 +44,7 @@ export class HttpClient extends Service<HttpClient> {
37
44
  this._requestInterceptors.push(fn);
38
45
  }
39
46
 
47
+ // Each interceptor gets its own clone, so reading the body there doesn't consume it.
40
48
  public addResponseInterceptor(fn: (response: Response) => void): void {
41
49
  this._responseInterceptors.push(fn);
42
50
  }
@@ -44,41 +52,45 @@ export class HttpClient extends Service<HttpClient> {
44
52
  protected onDestroy(): void {
45
53
  this._baseUrl = '';
46
54
  this._defaultHeaders = {};
55
+ this._trustedAPIs = [];
47
56
  this._requestInterceptors = [];
48
57
  this._responseInterceptors = [];
49
58
  }
50
59
 
51
60
  private async request<T>(
52
- method: HttpMethod,
61
+ method: Nutin.HttpMethod,
53
62
  endpoint: string,
54
63
  data?: unknown,
55
- config?: IRequestConfig
64
+ config: Nutin.RequestConfig = {}
56
65
  ): Promise<T> {
57
- const { controller, timeoutId } = HttpManager.createAbortController(config?.timeout);
58
- const fullUrl = `${this._baseUrl}${endpoint}`;
59
- const mergedConfig: IRequestConfig = {
60
- ...config,
61
- headers: { ...this._defaultHeaders, ...config?.headers },
62
- };
66
+ const abort = HttpManager.createAbortController(config.timeout, config.signal);
63
67
 
64
68
  try {
65
- const url = HttpBuilder.buildRequestUrl(fullUrl, config?.queryParams);
66
- const requestOptions = HttpBuilder.buildRequestOptions(method, data, mergedConfig, controller.signal);
69
+ const url = HttpBuilder.resolveUrl(this._baseUrl, endpoint, this._trustedAPIs);
70
+ HttpBuilder.appendQueryParams(url, config.queryParams);
71
+
72
+ // Without a base URL, the client's default headers only go to the page's own origin;
73
+ // per-call headers are sent wherever the call goes.
74
+ const sendDefaults = this._baseUrl !== '' || url.origin === window.location.origin;
75
+ const headers = HttpBuilder.mergeHeaders(sendDefaults ? this._defaultHeaders : undefined, config.headers);
76
+ const requestOptions = HttpBuilder.buildRequestOptions(method, data, config, headers, abort.controller.signal);
67
77
 
68
78
  this._requestInterceptors.forEach(fn => fn(url.toString(), requestOptions));
69
79
 
70
80
  const response = await fetch(url.toString(), requestOptions);
81
+ // Checked before interceptors: another origin's response is never handed to app code.
82
+ if (response.redirected) HttpBuilder.assertRedirectTarget(this._baseUrl, url, response.url);
71
83
 
72
- this._responseInterceptors.forEach(fn => fn(response));
84
+ this._responseInterceptors.forEach(fn => fn(response.clone()));
73
85
 
74
86
  await HttpManager.validateResponse(response);
75
87
  return await HttpManager.parseSuccessResponse<T>(response);
76
88
  } catch (error) {
77
- HttpManager.handleRequestError(error);
89
+ HttpManager.handleRequestError(error, abort.timedOut());
78
90
  } finally {
79
- HttpManager.cleanupTimeout(timeoutId);
91
+ abort.cleanup();
80
92
  }
81
93
  }
82
94
  }
83
95
 
84
- export const AppHttpClient = HttpClient.getInstance();
96
+ export const AppHttpClient = /* @__PURE__ */ HttpClient.getInstance();
@@ -1,4 +1,4 @@
1
- import { I18nService, AppEventBus } from '#root/dist/src/core/services/index.js';
1
+ import { I18nService, initI18n } from '#root/dist/src/core/services/index.js';
2
2
  import { CONFIG } from '#root/dist/src/core/config.js';
3
3
 
4
4
  class FetchMock {
@@ -57,7 +57,6 @@ describe('i18n module', async () => {
57
57
  });
58
58
  afterEach(() => {
59
59
  fetchMock.restore();
60
- localStorage.removeItem(I18nService.localStorageKey)
61
60
  });
62
61
 
63
62
  beforeAll(() =>{
@@ -125,23 +124,38 @@ describe('i18n module', async () => {
125
124
  expect(I18nService.translate('non.existent.key')).toBe('non.existent.key');
126
125
  });
127
126
 
127
+ it('translate and getTranslationObject ignore inherited Object.prototype members', () => {
128
+ expect(I18nService.translate('constructor')).toBe('constructor');
129
+ expect(I18nService.translate('__proto__')).toBe('__proto__');
130
+ expect(I18nService.translate('home.toString')).toBe('home.toString');
131
+ expect(I18nService.getTranslationObject('constructor')).toBe(null);
132
+ });
133
+
128
134
  it('setCurrentLanguage updates the language, persists it and loads its translations', async () => {
129
135
  await I18nService.setCurrentLanguage('fr');
130
136
 
131
137
  expect(I18nService.currentLanguage).toBe('fr');
132
138
  expect(I18nService.translate('home.title')).toBe('Mon App');
133
- expect(localStorage.getItem(I18nService.localStorageKey)).toBe('fr');
134
139
  });
135
140
 
136
141
  it('setCurrentLanguage emits a language-changed event with the new language', async () => {
137
142
  const received = [];
138
- const callback = (data) => received.push(data);
139
- I18nService.onLanguageChange(callback);
143
+ const unsubscribe = I18nService.onLanguageChange((data) => received.push(data));
140
144
 
141
145
  await I18nService.setCurrentLanguage('fr');
142
146
 
143
147
  expect(received).toEqual([{ lang: 'fr' }]);
144
- AppEventBus.off('language-changed', callback);
148
+ unsubscribe();
149
+ });
150
+
151
+ it('onLanguageChange returns a function that stops further notifications', async () => {
152
+ const received = [];
153
+ const unsubscribe = I18nService.onLanguageChange((data) => received.push(data));
154
+ unsubscribe();
155
+
156
+ await I18nService.setCurrentLanguage('fr');
157
+
158
+ expect(received).toEqual([]);
145
159
  });
146
160
 
147
161
  it('getTranslationObject returns the nested value for the current language', async () => {
@@ -224,12 +238,6 @@ describe('i18n module', async () => {
224
238
  errSpy.restore();
225
239
  });
226
240
 
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
241
  it('loadTranslations logs and leaves the language unchanged when the fetch rejects', async () => {
234
242
  Object.defineProperty(navigator, 'language', { writable: true, configurable: true, value: 'en-US' });
235
243
  const errSpy = spyOn(console, 'error');
@@ -258,13 +266,46 @@ describe('i18n module', async () => {
258
266
  errSpy.restore();
259
267
  });
260
268
 
261
- it('onDestroy resets translations and disposes the instance', async () => {
262
- await I18nService.initTranslations();
263
- expect(I18nService.translate('home.title')).toBe('My App');
269
+ it('onDestroy clears translations but keeps the saved language preference', async () => {
270
+ await I18nService.setCurrentLanguage('fr');
271
+ expect(I18nService.translate('home.title')).toBe('Mon App');
264
272
 
265
273
  I18nService.onDestroy();
266
274
 
267
275
  expect(I18nService.translate('home.title')).toBe('home.title');
268
- expect(localStorage.getItem(I18nService.localStorageKey)).toBe(null);
276
+ });
277
+
278
+ it('resetTranslations also forgets the saved language preference', async () => {
279
+ await I18nService.setCurrentLanguage('fr');
280
+
281
+ I18nService.resetTranslations();
282
+
283
+ expect(I18nService.currentLanguage).toBe('en');
284
+ });
285
+
286
+ it('initI18n loads the translations when CONFIG.i18n is enabled', async () => {
287
+ const original = I18nService.initTranslations;
288
+ let calls = 0;
289
+ I18nService.initTranslations = async () => { calls++; };
290
+ CONFIG.i18n = true;
291
+ try {
292
+ await initI18n();
293
+ expect(calls).toBe(1);
294
+ } finally {
295
+ CONFIG.i18n = false;
296
+ I18nService.initTranslations = original;
297
+ }
298
+ });
299
+
300
+ it('initI18n does nothing when CONFIG.i18n is disabled (default)', async () => {
301
+ const original = I18nService.initTranslations;
302
+ let calls = 0;
303
+ I18nService.initTranslations = async () => { calls++; };
304
+ try {
305
+ await initI18n();
306
+ expect(calls).toBe(0);
307
+ } finally {
308
+ I18nService.initTranslations = original;
309
+ }
269
310
  });
270
311
  });
@@ -1,49 +1,46 @@
1
- import { DEFAULT_LANGUAGE, LANGUAGES, Language, Translations } from './languages.js';
2
- import { AppEventBus, Service } from '../../index.js';
1
+ import { DEFAULT_LANGUAGE, LANGUAGES } from './languages.js';
2
+ import { AppEventBus } from '../event-bus/event-bus.js';
3
+ import { Service } from '../../base-classes/service/service.js';
3
4
  import { CONFIG } from '../../config.js';
4
5
 
5
6
  export class I18n extends Service<I18n> {
6
- private readonly _DEFAULT_LANGUAGE: Language = DEFAULT_LANGUAGE;
7
+ private readonly _DEFAULT_LANGUAGE: Nutin.Language = DEFAULT_LANGUAGE;
7
8
  private readonly _LANGUAGES = LANGUAGES;
8
- private _currentLanguage: Language = this._DEFAULT_LANGUAGE;
9
- private _translations: Translations = {};
10
- private _defaultTranslations: Translations = {};
11
- private readonly _localStorageKey = 'nutin-fav-lang';
9
+ private _currentLanguage: Nutin.Language = this._DEFAULT_LANGUAGE;
10
+ private _translations: Nutin.Translations = {};
11
+ private _defaultTranslations: Nutin.Translations = {};
12
12
 
13
13
  constructor() {
14
14
  super();
15
- this.registerCleanup(this.resetTranslations);
15
+ this.registerCleanup(this.clearTranslations);
16
16
  this._currentLanguage = this.getPreferredLanguage();
17
17
  }
18
18
 
19
- public get currentLanguage(): Language {
19
+ public get currentLanguage(): Nutin.Language {
20
20
  return this._currentLanguage;
21
21
  }
22
22
 
23
- public get defaultLanguage(): Language {
23
+ public get defaultLanguage(): Nutin.Language {
24
24
  return this._DEFAULT_LANGUAGE;
25
25
  }
26
26
 
27
- public get languages(): Language[] {
27
+ public get languages(): Nutin.Language[] {
28
28
  return this._LANGUAGES;
29
29
  }
30
30
 
31
- public get localStorageKey(): string {
32
- return this._localStorageKey;
33
- }
34
-
35
- public async setCurrentLanguage(lang: Language): Promise<void> {
31
+ public async setCurrentLanguage(lang: Nutin.Language): Promise<void> {
36
32
  this._currentLanguage = lang;
37
- this.savePreferences();
38
33
  await this.loadTranslations(lang);
39
34
  AppEventBus.emit('language-changed', { lang });
40
35
  }
41
36
 
42
- public onLanguageChange(callback: (payload: { lang: string }) => void) {
37
+ // Returns the unsubscribe function, like Navigation.onNavigate().
38
+ public onLanguageChange(callback: (payload: { lang: string }) => void): () => void {
43
39
  AppEventBus.subscribe('language-changed', callback);
40
+ return () => AppEventBus.off('language-changed', callback);
44
41
  }
45
42
 
46
- public async loadTranslations(lang: Language): Promise<void> {
43
+ public async loadTranslations(lang: Nutin.Language): Promise<void> {
47
44
  try {
48
45
  const response = await fetch(`/locales/${lang}.json`);
49
46
 
@@ -56,8 +53,7 @@ export class I18n extends Service<I18n> {
56
53
  await this.loadDefaultTranslations();
57
54
  }
58
55
 
59
- document.documentElement.lang = lang;
60
- this.savePreferences();
56
+ document.documentElement.lang = lang;
61
57
  } catch (error) {
62
58
  console.error('Translation load error:', error);
63
59
  }
@@ -101,31 +97,33 @@ export class I18n extends Service<I18n> {
101
97
  return value || null;
102
98
  }
103
99
 
100
+ // Also forgets the saved language preference.
104
101
  public resetTranslations(): void {
105
- this._translations = {};
106
- this._defaultTranslations = {};
107
- localStorage.removeItem(this._localStorageKey);
108
- this._currentLanguage = this._DEFAULT_LANGUAGE;
102
+ this.clearTranslations();
109
103
  }
110
104
 
105
+ // Teardown keeps the saved preference: it belongs to the user, not to this instance.
111
106
  protected onDestroy(): void | Promise<void> {
112
- this.resetTranslations();
113
107
  this.dispose();
114
108
  }
115
109
 
116
- private getPreferredLanguage(): Language {
110
+ private clearTranslations(): void {
111
+ this._translations = {};
112
+ this._defaultTranslations = {};
113
+ this._currentLanguage = this._DEFAULT_LANGUAGE;
114
+ }
115
+
116
+ private getPreferredLanguage(): Nutin.Language {
117
117
  const urlLocale = this.getLocaleFromUrl();
118
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;
119
+ const browserLang = navigator.language.split('-')[0] as Nutin.Language;
122
120
  return this._LANGUAGES.includes(browserLang) ? browserLang : this._DEFAULT_LANGUAGE;
123
121
  }
124
122
 
125
- private getLocaleFromUrl(): Language | null {
126
- if (!CONFIG.i18n) return null;
123
+ private getLocaleFromUrl(): Nutin.Language | null {
124
+ if (!(globalThis.__NUTIN_I18N__ ?? CONFIG.i18n)) return null;
127
125
 
128
- const first = window.location.pathname.split('/').filter(Boolean)[0] as Language;
126
+ const first = window.location.pathname.split('/').filter(Boolean)[0] as Nutin.Language;
129
127
  return first && this._LANGUAGES.includes(first) ? first : null;
130
128
  }
131
129
 
@@ -143,20 +141,22 @@ export class I18n extends Service<I18n> {
143
141
 
144
142
  private getNestedValue(obj: any, keys: string[]): any {
145
143
  let value = obj;
144
+ // Own keys only: "constructor" or "__proto__" must not resolve to Object.prototype members.
146
145
  for (const key of keys) {
147
- value = value?.[key];
146
+ value = value !== null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key)
147
+ ? value[key]
148
+ : undefined;
148
149
  if (!value) break;
149
150
  }
150
151
  return value;
151
152
  }
153
+ }
152
154
 
153
- private savePreferences(): void {
154
- localStorage.setItem(this._localStorageKey, this._currentLanguage);
155
- }
155
+ export const I18nService = /* @__PURE__ */ I18n.getInstance();
156
156
 
157
- private getPreferences(): string | null {
158
- return localStorage.getItem(this._localStorageKey);
159
- }
157
+ /**
158
+ * Loads the translations when i18n is enabled in nutin.config.js; a no-op otherwise.
159
+ */
160
+ export async function initI18n(): Promise<void> {
161
+ if (globalThis.__NUTIN_I18N__ ?? CONFIG.i18n) await I18nService.initTranslations();
160
162
  }
161
-
162
- export const I18nService = I18n.getInstance();
@@ -3,5 +3,3 @@ import { CONFIG } from '../../config.js';
3
3
  export const LANGUAGES = CONFIG.langs.languages;
4
4
  export const DEFAULT_LANGUAGE = CONFIG.langs.defaultLanguage as (typeof LANGUAGES)[number];
5
5
 
6
- export type Language = typeof LANGUAGES[number];
7
- export type Translations = Record<string, any>;
@@ -1,13 +1,11 @@
1
- import { Service } from "../../base-classes/index.js";
2
-
3
- export type PipeFunction = (value: any, ...args: any[]) => string;
1
+ import { Service } from "../../base-classes/service/service.js";
4
2
 
5
3
  class PipeRegistry extends Service<PipeRegistry> {
6
- private _pipes: Record<string, PipeFunction> = {};
4
+ private _pipes: Record<string, Nutin.PipeFunction> = {};
7
5
 
8
6
  constructor() { super(); }
9
7
 
10
- public register(name: string, fn: PipeFunction): void {
8
+ public register(name: string, fn: Nutin.PipeFunction): void {
11
9
  if(this._pipes[name]) {
12
10
  console.warn(`Pipe "${name}" already exists - skipping.`);
13
11
  return;
@@ -29,4 +27,4 @@ class PipeRegistry extends Service<PipeRegistry> {
29
27
  }
30
28
  }
31
29
 
32
- export const AppPipeRegistry = PipeRegistry.getInstance();
30
+ export const AppPipeRegistry = /* @__PURE__ */ PipeRegistry.getInstance();
@@ -52,6 +52,16 @@ describe('pipes (registerPipes)', () => {
52
52
  expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', true])).toBe(expected);
53
53
  });
54
54
 
55
+ it('date reads the time flag from template strings: "true" adds the time, "false" does not', () => {
56
+ const value = '2024-03-15T14:30:00Z';
57
+ const dateOnly = new Date(value).toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
58
+ const withTime = new Date(value).toLocaleString('en-US', {
59
+ year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
60
+ });
61
+ expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', 'false'])).toBe(dateOnly);
62
+ expect(AppPipeRegistry.apply('date', value, ['en-US', 'long', 'true'])).toBe(withTime);
63
+ });
64
+
55
65
  it('date warns and returns the raw stringified value for an invalid date', () => {
56
66
  const warnSpy = spyOn(console, 'warn');
57
67
  warnSpy.andCallFake(() => {});
@@ -18,12 +18,14 @@ export const registerPipes = (): void => {
18
18
  value: string | number | Date,
19
19
  locale = navigator.language,
20
20
  format = 'long',
21
- time = false,
21
+ time: boolean | string = false,
22
22
  ) => {
23
23
  const date = new Date(value);
24
24
  // '' is not a valid Intl.DateTimeFormatOptions value for hour/minute, so
25
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' } : {};
26
+ // From a template the flag arrives as a string ("false" is truthy).
27
+ const withTime = time === true || time === 'true';
28
+ const timeOptions: Intl.DateTimeFormatOptions = withTime ? { hour: '2-digit', minute: '2-digit' } : {};
27
29
  const formats = {
28
30
  short: { year: 'numeric', month: 'short', day: 'numeric', ...timeOptions } as Intl.DateTimeFormatOptions,
29
31
  long: { year: 'numeric', month: 'long', day: 'numeric', ...timeOptions } as Intl.DateTimeFormatOptions,