@nutin/cli 2.1.1 → 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 (229) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/README.md +13 -5
  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 +9 -8
  34. package/templates/base/AGENTS.md.hbs +42 -41
  35. package/templates/base/GETTING_STARTED.md.hbs +26 -20
  36. package/templates/base/README.md.hbs +7 -4
  37. package/templates/base/src/app/globals.d.ts.hbs +6 -3
  38. package/templates/base/src/app/guards.ts.hbs +1 -3
  39. package/templates/base/src/app/main.ts.hbs +2 -9
  40. package/templates/base/src/app/routes.ts.hbs +7 -2
  41. package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
  42. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  43. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
  44. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
  45. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
  46. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
  47. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
  48. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +286 -24
  49. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
  50. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +66 -4
  51. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
  52. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
  53. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +93 -54
  54. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
  55. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
  57. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
  58. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
  59. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
  60. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
  61. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
  62. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
  63. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
  64. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
  65. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
  66. package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
  67. package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
  68. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
  69. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
  70. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
  71. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
  72. package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
  73. package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
  74. package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
  75. package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
  76. package/templates/base/src/core/index.ts.hbs +4 -2
  77. package/templates/base/src/core/internals.d.ts.hbs +199 -20
  78. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
  79. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
  80. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
  81. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
  82. package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
  83. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
  84. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
  85. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
  86. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
  87. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
  88. package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
  89. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
  90. package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
  91. package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
  92. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
  93. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
  94. package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
  95. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +183 -4
  96. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
  97. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
  98. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
  99. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
  100. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
  101. package/templates/base/src/core/services/router/router.test.js.hbs +306 -0
  102. package/templates/base/src/core/services/router/router.ts.hbs +113 -53
  103. package/templates/base/tools/builder/builder.config.js.hbs +6 -0
  104. package/templates/base/tools/builder/builder.js.hbs +18 -6
  105. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
  106. package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
  107. package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
  108. package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
  109. package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
  110. package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
  111. package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
  112. package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
  113. package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
  114. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
  115. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
  116. package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
  117. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
  118. package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
  119. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
  120. package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
  121. package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
  122. package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
  123. package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
  124. package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
  125. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
  126. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +15 -7
  127. package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
  128. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
  129. package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
  130. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
  131. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +43 -13
  132. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
  133. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
  134. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +28 -21
  135. package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
  136. package/templates/base/tools/builder/core/seo/route-output.js.hbs +24 -6
  137. package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
  138. package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
  139. package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
  140. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
  141. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
  142. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
  143. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +24 -11
  144. package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
  145. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
  146. package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
  147. package/templates/base/tools/dev/serve.js.hbs +152 -22
  148. package/templates/base/tools/dev/watcher.js.hbs +34 -3
  149. package/templates/base/tools/generator/generator.js.hbs +1 -1
  150. package/templates/base/tools/generator/handle-file.js.hbs +4 -2
  151. package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
  152. package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
  153. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
  154. package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
  155. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
  156. package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
  157. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  158. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
  159. package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
  160. package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
  161. package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
  162. package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
  163. package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
  164. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
  165. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
  166. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
  167. package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
  168. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
  169. package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
  170. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
  171. package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
  172. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
  173. package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
  174. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
  175. package/templates/base/tools/utils/build-lock.js.hbs +30 -2
  176. package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
  177. package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
  178. package/templates/base/tools/utils/format.js.hbs +1 -1
  179. package/templates/base/tools/utils/index.js.hbs +2 -1
  180. package/templates/base/tools/utils/print.js.hbs +2 -2
  181. package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
  182. package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
  183. package/templates/base/tools/utils/run-command.js.hbs +30 -1
  184. package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
  185. package/templates/base/tools/utils/run-script.js.hbs +2 -2
  186. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +3 -0
  187. package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
  188. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
  189. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
  190. package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
  191. package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
  192. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
  193. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
  194. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
  195. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
  196. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +21 -4
  197. package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
  198. package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
  199. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
  200. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
  201. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
  202. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
  203. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
  204. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
  205. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
  206. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
  207. package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
  208. package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
  209. package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
  210. package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
  211. package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
  212. package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
  213. package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
  214. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
  215. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
  216. package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
  217. package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
  218. package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
  219. package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
  220. package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
  221. package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
  222. package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
  223. package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
  224. package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
  225. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
  226. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
  227. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
  228. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
  229. /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
@@ -1,13 +1,9 @@
1
- import { BaseComponent, BaseComponentOptions } from '../../index.js';
2
-
3
- export interface ViewOptions extends BaseComponentOptions {
4
- viewName: string;
5
- }
1
+ import { BaseComponent } from '../base-component/base-component.js';
6
2
 
7
3
  export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComponent<T> {
8
4
  private _viewName: string;
9
5
  protected routeParams: Record<string, string> = {};
10
- private _template: string;
6
+ private _template: Nutin.Template;
11
7
 
12
8
  constructor({
13
9
  template,
@@ -15,7 +11,7 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
15
11
  mountTarget = '#app',
16
12
  viewName,
17
13
  trustLevel,
18
- }: ViewOptions) {
14
+ }: Nutin.ViewOptions) {
19
15
  super({ mountTarget, tagName, trustLevel });
20
16
  if (!viewName) throw new Error('View requires a viewName.');
21
17
  this._template = template ?? '';
@@ -26,7 +22,7 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
26
22
  return this._viewName;
27
23
  }
28
24
 
29
- protected override generateTemplate(): string {
25
+ protected override generateTemplate(): Nutin.Template {
30
26
  return this._template;
31
27
  }
32
28
 
@@ -46,6 +42,13 @@ export abstract class View<T extends HTMLElement = HTMLElement> extends BaseComp
46
42
  return key in this.routeParams && this.routeParams[key] !== undefined;
47
43
  }
48
44
 
45
+ // The page's title, read by the router after each navigation (document.title). Override it
46
+ // for a title that depends on the route, e.g. the page being shown. Undefined: the router
47
+ // uses config/seo.json's title, then the "<viewName>.title" translation, then viewName.
48
+ public documentTitle(): string | undefined {
49
+ return undefined;
50
+ }
51
+
49
52
  // Navigation hooks — called by router only, never by render lifecycle
50
53
  public onEnter(): void {}
51
54
  public onExit(): void {}
@@ -1,5 +1,7 @@
1
- // Abstract classes
1
+ // Abstract classes and helpers:
2
+ // Component, View, Service
2
3
  export * from './base-classes/index.js';
3
4
 
4
- // Core functionalities
5
+ // Core functionalities:
6
+ // EventBus and facades, global components, httpClient, i18n, pipeRegistry, router
5
7
  export * from './services/index.js';
@@ -1,27 +1,206 @@
1
- declare interface NavigationEventMap {
2
- 'navigate': { path: string };
3
- 'reload': {};
4
- }
1
+ // Nutin's public types, all inside the global `Nutin` namespace so they can't collide with other packages' globals.
2
+ // No import needed: the app extends it in src/app/globals.d.ts.
3
+ // This file must stay free of top-level import/export, so runtime classes go through import().
4
+ declare namespace Nutin {
5
+ interface RouterEventMap {
6
+ 'navigate': { path: string };
7
+ 'reload': undefined;
8
+ }
5
9
 
6
- declare interface LifecycleEventMap {
7
- 'before-render': {};
8
- 'after-render': {};
9
- 'before-destroy': {};
10
- 'after-destroy': {};
11
- 'view-mount': { viewName: string };
12
- 'view-unmount': { viewName: string };
13
- }
10
+ interface LifecycleEventMap {
11
+ 'before-render': undefined;
12
+ 'after-render': undefined;
13
+ 'before-destroy': undefined;
14
+ 'after-destroy': undefined;
15
+ 'view-mount': { viewName: string };
16
+ 'view-unmount': { viewName: string };
17
+ }
18
+
19
+ interface I18nEventMap {
20
+ 'language-changed': {
21
+ lang: string;
22
+ };
23
+ }
24
+
25
+ interface FrameworkEventMap extends RouterEventMap, LifecycleEventMap, I18nEventMap {}
26
+
27
+ interface AppEventMap {}
28
+
29
+ interface EventMap extends FrameworkEventMap, AppEventMap {}
14
30
 
15
- declare interface I18nEventMap {
16
- 'language-changed': {
17
- lang: string;
31
+ type EventKey = keyof EventMap;
32
+
33
+ type Subscription<K extends EventKey = EventKey> = {
34
+ event: K;
35
+ callback: (data: EventMap[K]) => void;
36
+ once?: boolean;
18
37
  };
19
- }
20
38
 
21
- declare interface FrameworkEventMap extends NavigationEventMap, LifecycleEventMap, I18nEventMap {}
39
+ // Components
40
+
41
+ type Template = string | import('./index.js').SafeHtml;
42
+
43
+ type TrustLevel = 'strict' | 'normal' | 'trusted';
44
+
45
+ interface ComponentConfig {
46
+ selector: string;
47
+ factory: (element: HTMLElement) => import('./index.js').Component;
48
+ // Same keep value on the next render: the child is kept as-is instead of recreated
49
+ keep?: string | number;
50
+ }
51
+
52
+ interface BaseComponentOptions {
53
+ template?: Template;
54
+ mountTarget?: string | HTMLElement;
55
+ tagName?: keyof HTMLElementTagNameMap;
56
+ trustLevel?: TrustLevel;
57
+ }
58
+
59
+ interface ComponentProps {
60
+ // Common HTML attributes - extend as needed
61
+ className?: string;
62
+ style?: string;
63
+ textContent?: string;
64
+
65
+ // Form field bindings - extend as needed
66
+ name?: string;
67
+ email?: string;
68
+
69
+ // Allow for additional data-bind attributes
70
+ [key: string]: any;
71
+ }
72
+
73
+ interface ComponentOptions<K = any> extends BaseComponentOptions {
74
+ props?: ComponentProps;
75
+ config?: K;
76
+ defaults?: Partial<K>;
77
+ templateFn?: (config?: K) => Template;
78
+ normalizeKeys?: (keyof K)[];
79
+ }
80
+
81
+ interface ViewOptions extends BaseComponentOptions {
82
+ viewName: string;
83
+ }
84
+
85
+ interface CatalogConfig extends ComponentOptions {
86
+ items: CatalogItemConfig[];
87
+ elementName: string;
88
+ elementTag?: keyof HTMLElementTagNameMap;
89
+ selector: string;
90
+ component: new (el: HTMLElement, data: any, props?: any) => import('./index.js').Component;
91
+ // Same value on the next render keeps an unchanged item as-is; changed items are recreated
92
+ keepBy?: (item: any, index: number) => string | number;
93
+ }
94
+
95
+ interface CatalogItemBase {
96
+ index: number;
97
+ }
98
+
99
+ type CatalogItemObject<T extends object> = T & CatalogItemBase;
100
+
101
+ interface CatalogItemPrimitive extends CatalogItemBase {
102
+ value: string | number | boolean | null | undefined;
103
+ }
22
104
 
23
- declare interface AppEventMap {}
105
+ type CatalogItemConfig<T = any> =
106
+ T extends object ? CatalogItemObject<T> : CatalogItemPrimitive;
24
107
 
25
- declare interface EventMap extends FrameworkEventMap, AppEventMap {}
108
+ // Router
109
+
110
+ /**
111
+ * Route guard function that receives route parameters and returns:
112
+ * - true to allow navigation
113
+ * - false to block it
114
+ * - string to redirect to a different route
115
+ */
116
+ type RouteGuard = (params: Record<string, string>) => boolean | string | Promise<boolean | string>;
117
+
118
+ /**
119
+ * Creates a route's view. Returning a Promise makes the route lazy: e.g.
120
+ * `() => import('./views/about/about.view.js').then((m) => new m.AboutView())`
121
+ * puts the view's code in its own chunk, loaded on the first visit.
122
+ */
123
+ type ViewFactory = () => import('./index.js').View | Promise<import('./index.js').View>;
124
+
125
+ /**
126
+ * Route configuration - can be just a view factory or an object with guards
127
+ */
128
+ type RouteConfig = ViewFactory | {
129
+ view: ViewFactory;
130
+ guards?: RouteGuard[];
131
+ };
132
+
133
+ type Routes = Record<string, RouteConfig>;
134
+
135
+ interface RouteMatch {
136
+ route: RouteConfig;
137
+ params: Record<string, string>;
138
+ pattern: string;
139
+ }
140
+
141
+ // Instance types of the framework services
142
+ type Router = ReturnType<typeof import('./index.js').AppRouter>;
143
+ type EventBus = InstanceType<typeof import('./index.js').EventBus>;
144
+ type HttpClient = InstanceType<typeof import('./index.js').HttpClient>;
145
+
146
+ // HTTP
147
+
148
+ type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
149
+
150
+ type QueryValue = string | number | boolean | null | undefined;
151
+
152
+ /**
153
+ * headers?: Record<string, string>;
154
+ * queryParams?: Record<string, QueryValue | QueryValue[]>;
155
+ * timeout?: number;
156
+ * signal?: AbortSignal;
157
+ * credentials? / cache? / referrerPolicy? / redirect?: passed to fetch as-is.
158
+ */
159
+ interface RequestConfig {
160
+ headers?: Record<string, string>;
161
+ queryParams?: Record<string, QueryValue | QueryValue[]>;
162
+ timeout?: number;
163
+ signal?: AbortSignal;
164
+ credentials?: RequestCredentials;
165
+ cache?: RequestCache;
166
+ referrerPolicy?: ReferrerPolicy;
167
+ redirect?: RequestRedirect;
168
+ }
169
+
170
+ interface HttpClientOptions {
171
+ // URL prefixes (origin + path) whose paths may contain encoded "/" or "\" (%2F, %5C).
172
+ trustedAPIs?: string[];
173
+ }
174
+
175
+ // i18n & pipes
176
+
177
+ type Language = typeof import('./services/i18n/languages.js').LANGUAGES[number];
178
+ type Translations = Record<string, any>;
179
+
180
+ type PipeFunction = (value: any, ...args: any[]) => string;
181
+
182
+ // Globals
183
+
184
+ interface GlobalMountable {
185
+ render(): HTMLElement;
186
+ destroy?(): void;
187
+ }
188
+
189
+ interface GlobalConfig<T extends GlobalMountable = GlobalMountable> {
190
+ /** The component's own class — constructed internally as `new component(mountTarget)`. */
191
+ component: new (mountTarget: HTMLElement) => T;
192
+ /** Stamped onto the mounted root element; pass this same id to `hideGlobals`/`revealGlobals`. */
193
+ id: string;
194
+ }
195
+
196
+ interface RegisterGlobalsOptions {
197
+ /** Rendered and prepended to `<body>`, in the given order (e.g. a header). */
198
+ before?: GlobalConfig[];
199
+ /** Rendered and appended to `<body>`, in the given order (e.g. a footer). */
200
+ after?: GlobalConfig[];
201
+ }
202
+ }
26
203
 
27
- declare type EventKey = keyof EventMap;
204
+ // Set by the builder's esbuild `define` so prod bundles can drop i18n code when it's
205
+ // disabled; undefined in tests and tsc output, which fall back to CONFIG.i18n.
206
+ declare var __NUTIN_I18N__: boolean | undefined;
@@ -175,6 +175,24 @@ describe('EventBus', () => {
175
175
  expect(regularCalls).toBe(2);
176
176
  });
177
177
 
178
+ it('keeps dispatching to the other listeners when one throws, and still removes a throwing once() handler', () => {
179
+ const errSpy = spyOn(console, 'error');
180
+ errSpy.andCallFake(() => {});
181
+ let received = 0;
182
+ let onceCalls = 0;
183
+
184
+ eventBus.once('notification', () => { onceCalls++; throw new Error('boom'); });
185
+ eventBus.subscribe('notification', () => { received++; });
186
+
187
+ eventBus.emit('notification', {});
188
+ eventBus.emit('notification', {});
189
+
190
+ expect(onceCalls).toBe(1);
191
+ expect(received).toBe(2);
192
+ expect(errSpy.callCount).toBe(1);
193
+ errSpy.restore();
194
+ });
195
+
178
196
  it('off(event) with no callback removes every handler for that event', () => {
179
197
  let callsA = 0;
180
198
  let callsB = 0;
@@ -1,15 +1,10 @@
1
- import { Service } from "../../index.js";
1
+ import { Service } from "../../base-classes/service/service.js";
2
2
 
3
- export type IEventBus = InstanceType<typeof EventBus>;
4
-
5
- export type Subscription<K extends EventKey = EventKey> = {
6
- event: K;
7
- callback: (data: EventMap[K]) => void;
8
- once?: boolean;
9
- };
3
+ // The payload is required unless the event declares none (`undefined` in its EventMap entry).
4
+ type EventArgs<K extends Nutin.EventKey> = undefined extends Nutin.EventMap[K] ? [data?: Nutin.EventMap[K]] : [data: Nutin.EventMap[K]];
10
5
 
11
6
  export class EventBus extends Service<EventBus> {
12
- private _subscriptions: Subscription[] = [];
7
+ private _subscriptions: Nutin.Subscription[] = [];
13
8
 
14
9
  constructor() {
15
10
  super();
@@ -17,29 +12,35 @@ export class EventBus extends Service<EventBus> {
17
12
  }
18
13
 
19
14
  private handlers: {
20
- [K in EventKey]?: Array<(data: EventMap[K]) => void>;
15
+ [K in Nutin.EventKey]?: Array<(data: Nutin.EventMap[K]) => void>;
21
16
  } = {};
22
17
 
23
- public subscribe<K extends EventKey>(
18
+ public subscribe<K extends Nutin.EventKey>(
24
19
  event: K,
25
- callback: (data: EventMap[K]) => void
20
+ callback: (data: Nutin.EventMap[K]) => void
26
21
  ): void {
27
22
  this.addHandler(event, callback, false);
28
23
  }
29
24
 
30
- public once<K extends EventKey>(
25
+ public once<K extends Nutin.EventKey>(
31
26
  event: K,
32
- callback: (data: EventMap[K]) => void
27
+ callback: (data: Nutin.EventMap[K]) => void
33
28
  ): void {
34
29
  this.addHandler(event, callback, true);
35
30
  }
36
31
 
37
- public emit<K extends EventKey>(event: K, data?: EventMap[K]): void {
32
+ public emit<K extends Nutin.EventKey>(event: K, ...[data]: EventArgs<K>): void {
38
33
  const callbacks = this.handlers[event];
39
34
  if (!callbacks) return;
40
35
 
41
36
  callbacks.slice().forEach(callback => {
42
- callback(data!);
37
+ // A failing listener is reported but doesn't keep the others (the router's
38
+ // included) from receiving the event.
39
+ try {
40
+ callback(data as Nutin.EventMap[K]);
41
+ } catch (error) {
42
+ console.error(`Error in a "${event}" listener:`, error);
43
+ }
43
44
 
44
45
  const subIndex = this._subscriptions.findIndex(
45
46
  sub => sub.event === event && sub.callback === callback && sub.once
@@ -50,17 +51,17 @@ export class EventBus extends Service<EventBus> {
50
51
  });
51
52
  }
52
53
 
53
- public off<K extends EventKey>(
54
+ public off<K extends Nutin.EventKey>(
54
55
  event: K,
55
- callback?: (data: EventMap[K]) => void
56
+ callback?: (data: Nutin.EventMap[K]) => void
56
57
  ): void {
57
58
  const handlers = this.handlers[event];
58
59
  if (!handlers) return;
59
60
 
60
61
  if (callback) {
61
62
  (this.handlers as Record<
62
- EventKey,
63
- Array<(data: EventMap[K]) => void>
63
+ Nutin.EventKey,
64
+ Array<(data: Nutin.EventMap[K]) => void>
64
65
  >)[event] = handlers.filter(h => h !== callback);
65
66
  } else {
66
67
  delete this.handlers[event];
@@ -71,18 +72,18 @@ export class EventBus extends Service<EventBus> {
71
72
  );
72
73
  }
73
74
 
74
- private addHandler<K extends EventKey>(
75
+ private addHandler<K extends Nutin.EventKey>(
75
76
  event: K,
76
- callback: (data: EventMap[K]) => void,
77
+ callback: (data: Nutin.EventMap[K]) => void,
77
78
  once: boolean
78
79
  ): void {
79
80
  if (!this.handlers[event]) {
80
81
  this.handlers[event] = [];
81
82
  }
82
83
  (
83
- this.handlers as Record<EventKey, Array<(data: EventMap[K]) => void>>
84
+ this.handlers as Record<Nutin.EventKey, Array<(data: Nutin.EventMap[K]) => void>>
84
85
  )[event].push(callback);
85
- this._subscriptions.push({ event, callback, once } as Subscription);
86
+ this._subscriptions.push({ event, callback, once } as Nutin.Subscription);
86
87
  }
87
88
 
88
89
  private cleanupEventListeners = (): void => {
@@ -93,4 +94,4 @@ export class EventBus extends Service<EventBus> {
93
94
  };
94
95
  }
95
96
 
96
- export const AppEventBus = EventBus.getInstance();
97
+ export const AppEventBus = /* @__PURE__ */ EventBus.getInstance();
@@ -1,7 +1,7 @@
1
- import { AppEventBus, IEventBus } from "./event-bus.js";
1
+ import { AppEventBus } from "./event-bus.js";
2
2
 
3
3
  class LifecycleFacade {
4
- constructor(private bus: IEventBus) {}
4
+ constructor(private bus: Nutin.EventBus) {}
5
5
 
6
6
  beforeRender(): void {
7
7
  this.bus.emit('before-render');
@@ -58,4 +58,4 @@ class LifecycleFacade {
58
58
  }
59
59
  }
60
60
 
61
- export const Lifecycle = new LifecycleFacade(AppEventBus);
61
+ export const Lifecycle = /* @__PURE__ */ new LifecycleFacade(AppEventBus);
@@ -1,7 +1,7 @@
1
- import { AppEventBus, IEventBus } from "./event-bus.js";
1
+ import { AppEventBus } from "./event-bus.js";
2
2
 
3
3
  class NavigationFacade {
4
- constructor(private bus: IEventBus) {}
4
+ constructor(private bus: Nutin.EventBus) {}
5
5
 
6
6
  navigateTo(path: string): void {
7
7
  this.bus.emit('navigate', { path });
@@ -22,4 +22,4 @@ class NavigationFacade {
22
22
  }
23
23
  }
24
24
 
25
- export const Navigation = new NavigationFacade(AppEventBus);
25
+ export const Navigation = /* @__PURE__ */ new NavigationFacade(AppEventBus);
@@ -1,41 +1,17 @@
1
- import { Service } from '../../base-classes/index.js';
2
-
3
- export interface GlobalMountable {
4
- render(): HTMLElement;
5
- destroy?(): void;
6
- }
7
-
8
- export interface GlobalConfig<T extends GlobalMountable = GlobalMountable> {
9
- /** The component's own class — constructed internally as `new component(mountTarget)`. */
10
- component: new (mountTarget: HTMLElement) => T;
11
- /** Stamped onto the mounted root element; pass this same id to `hideGlobals`/`revealGlobals`. */
12
- id: string;
13
- }
14
-
15
- export interface RegisterGlobalsOptions {
16
- /** Rendered and prepended to `<body>`, in the given order (e.g. a header). */
17
- before?: GlobalConfig[];
18
- /** Rendered and appended to `<body>`, in the given order (e.g. a footer). */
19
- after?: GlobalConfig[];
20
- }
1
+ import { Service } from '../../base-classes/service/service.js';
21
2
 
22
3
  export class Globals extends Service<Globals> {
23
- private _mounted: Record<string, GlobalMountable> = {};
4
+ private _mounted: Record<string, Nutin.GlobalMountable> = {};
24
5
  private _originalDisplay: Record<string, string> = {};
25
6
 
26
7
  constructor() { super(); }
27
8
 
28
- public register({ before = [], after = [] }: RegisterGlobalsOptions): void {
29
- // Component subclasses generated via `nutin generate component` type their
30
- // mountTarget param as HTMLElement, but DomHelper.mountElement only
31
- // *appends* (rather than replacing) the target when mountTarget is a string
32
- // selector. This cast satisfies the stricter constructor type while still
33
- // passing the string 'body' through at runtime, so components append into
34
- // <body> instead of replacing it.
9
+ public register({ before = [], after = [] }: Nutin.RegisterGlobalsOptions): void {
10
+ // Satisfies the stricter constructor type while letting components append into <body> instead of replacing it.
35
11
  const mountTarget = 'body' as unknown as HTMLElement;
36
12
  const isElement = (el: HTMLElement | null): el is HTMLElement => el !== null;
37
13
 
38
- const mount = ({ component: Component, id }: GlobalConfig): HTMLElement | null => {
14
+ const mount = ({ component: Component, id }: Nutin.GlobalConfig): HTMLElement | null => {
39
15
  if (this._mounted[id]) {
40
16
  console.warn(`Global "${id}" is already registered - skipping.`);
41
17
  return null;
@@ -80,13 +56,12 @@ export class Globals extends Service<Globals> {
80
56
  /**
81
57
  * Mounts components outside the router's render target (`<main id="app">`),
82
58
  * e.g. a header/footer that should persist across every route. Each entry's
83
- * `component` is constructed internally — callers never see or handle
84
- * `mountTarget` themselves — and its rendered root element is given the
59
+ * `component` is constructed internally and its rendered root element is given the
85
60
  * entry's `id`, so `hideGlobals(ids)`/`revealGlobals(ids)` can target it
86
61
  * later by that same id. Registering an `id` that's already registered logs
87
62
  * a `console.warn` and skips it.
88
63
  */
89
- export const registerGlobals = (options: RegisterGlobalsOptions): void => {
64
+ export const registerGlobals = (options: Nutin.RegisterGlobalsOptions): void => {
90
65
  Globals.getInstance().register(options);
91
66
  };
92
67