@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,136 @@
1
+ import path from 'path';
2
+ import * as fsExtra from 'fs-extra';
3
+ import inquirer from 'inquirer';
4
+ import { print } from '../utils/print.mjs';
5
+ import { PACKAGE_VERSION as packageVersion, META_FILE_NAME } from '../common/package-data.mjs';
6
+ import { readProjectMeta, updateProjectMeta } from '../common/project-meta.mjs';
7
+ import { fetchOldTemplates } from './fetch-old-templates.mjs';
8
+ import { parseVersion, compareVersions } from './version-compare.mjs';
9
+ import { bootstrapProjectMeta } from '../common/meta-bootstrap-prompt.mjs';
10
+ import { printUpdateSummary } from './update-summary-printer.mjs';
11
+ import { writeConflictReport } from './conflict-report-writer.mjs';
12
+ import { UpdateContextBuilder } from './update-context-builder.mjs';
13
+ import { TemplateDiffer } from './template-differ.mjs';
14
+
15
+ const fs = fsExtra.default;
16
+ const REPORT_FILE_NAME = 'NUTIN-UPDATE-REPORT.md';
17
+
18
+ export class ProjectUpdater {
19
+ constructor() {
20
+ this.contextBuilder = new UpdateContextBuilder();
21
+ this.differ = new TemplateDiffer();
22
+ }
23
+
24
+ async updateProject(projectPath, options = {}) {
25
+ const { yes = false, from } = options;
26
+ const meta = await this.resolveMeta(projectPath);
27
+
28
+ if (this.isUpToDate(meta)) {
29
+ print.boldSuccess(`✅ Already up to date (nutin v${packageVersion}).`);
30
+ return;
31
+ }
32
+ this.assertUpdatable(meta);
33
+
34
+ print.boldHead(`\n🔄 Updating ${projectPath} from nutin v${meta.version} to v${packageVersion}...\n`);
35
+
36
+ const { templatesRoot: oldTemplatesRoot, cleanup } = await fetchOldTemplates(meta.version, { from });
37
+
38
+ try {
39
+ const changeSet = await this.computeChangeSet(projectPath, meta, oldTemplatesRoot);
40
+ await this.applyChangeSet(projectPath, meta, changeSet, { yes });
41
+ } finally {
42
+ await cleanup();
43
+ }
44
+ }
45
+
46
+ async resolveMeta(projectPath) {
47
+ const meta = (await readProjectMeta(projectPath)) ?? (await bootstrapProjectMeta(projectPath));
48
+ this.assertValidVersion(meta.version);
49
+ return meta;
50
+ }
51
+
52
+ // meta.version ends up interpolated into a shell command in fetchOldTemplates
53
+ // (npm pack), so it must be strictly numeric before anything downstream uses it —
54
+ // .nutin-meta.json is a plain project file that could be hand-edited or come from
55
+ // an untrusted clone.
56
+ assertValidVersion(version) {
57
+ if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) {
58
+ throw new Error(
59
+ `${META_FILE_NAME} has an invalid version "${version}" — expected a version in major.minor.patch format, e.g. "1.3.1". ` +
60
+ `Fix or delete ${META_FILE_NAME} and try again.`,
61
+ );
62
+ }
63
+ }
64
+
65
+ isUpToDate(meta) {
66
+ return compareVersions(meta.version, packageVersion) === 0;
67
+ }
68
+
69
+ assertUpdatable(meta) {
70
+ const comparison = compareVersions(meta.version, packageVersion);
71
+ if (comparison > 0) {
72
+ throw new Error(
73
+ `This project records nutin v${meta.version}, but the installed CLI is v${packageVersion}. ` +
74
+ 'Install the latest nutin with "npm i -g @nutin/cli" before running nutin-update.',
75
+ );
76
+ }
77
+ if (parseVersion(meta.version).major !== parseVersion(packageVersion).major) {
78
+ throw new Error(
79
+ `nutin-update only handles minor/patch updates. Project is on v${meta.version}, installed CLI is ` +
80
+ `v${packageVersion} — that's a major version change. Please migrate manually (see the changelog).`,
81
+ );
82
+ }
83
+ }
84
+
85
+ async computeChangeSet(projectPath, meta, oldTemplatesRoot) {
86
+ const { oldContext, newContext } = await this.contextBuilder.buildContexts(projectPath, meta);
87
+ return this.differ.diff(projectPath, oldTemplatesRoot, oldContext, newContext);
88
+ }
89
+
90
+ async applyChangeSet(projectPath, meta, changeSet, { yes }) {
91
+ const { toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated } = changeSet;
92
+ printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated });
93
+
94
+ if (toUpdate.length === 0 && toAdd.length === 0 && conflicts.length === 0) {
95
+ print.boldSuccess('\n✅ Nothing to update.');
96
+ await updateProjectMeta(projectPath, { version: packageVersion });
97
+ return;
98
+ }
99
+
100
+ if (!yes && (toUpdate.length > 0 || toAdd.length > 0)) {
101
+ const { proceed } = await inquirer.prompt([
102
+ {
103
+ type: 'confirm',
104
+ name: 'proceed',
105
+ message: `\nApply ${toUpdate.length + toAdd.length} safe file update(s)?`,
106
+ default: true,
107
+ },
108
+ ]);
109
+ if (!proceed) {
110
+ print.boldInfo('Aborted — no files were changed.');
111
+ return;
112
+ }
113
+ }
114
+
115
+ for (const { relPath, newEntry } of [...toUpdate, ...toAdd]) {
116
+ const outputPath = path.join(projectPath, relPath);
117
+ await fs.ensureDir(path.dirname(outputPath));
118
+ await fs.writeFile(outputPath, newEntry.content);
119
+ }
120
+
121
+ if (conflicts.length > 0) {
122
+ await writeConflictReport(projectPath, conflicts, meta.version, packageVersion);
123
+ print.boldError(`\n⚠️ ${conflicts.length} file(s) you modified need manual attention — see ${REPORT_FILE_NAME}`);
124
+ }
125
+
126
+ await updateProjectMeta(projectPath, { version: packageVersion });
127
+
128
+ print.boldSuccess(`\n✅ Updated to nutin v${packageVersion}.`);
129
+ }
130
+ }
131
+
132
+ export const projectUpdater = new ProjectUpdater();
133
+
134
+ export async function updateProject(projectPath, options = {}) {
135
+ return projectUpdater.updateProject(projectPath, options);
136
+ }
@@ -0,0 +1,124 @@
1
+ import path from 'path';
2
+ import * as fsExtra from 'fs-extra';
3
+ import { FileGenerator } from '../common/file-generator.mjs';
4
+
5
+ const fs = fsExtra.default;
6
+
7
+ function contentEquals(a, b) {
8
+ if (Buffer.isBuffer(a) || Buffer.isBuffer(b)) {
9
+ return Buffer.compare(Buffer.from(a), Buffer.from(b)) === 0;
10
+ }
11
+ return a === b;
12
+ }
13
+
14
+ // Paths the user is expected to hand-edit right after `create-app` — their generated
15
+ // content is just placeholder/starter scaffolding, so nutin-update must never rewrite
16
+ // or conflict-flag them.
17
+ const FULLY_EXCLUDED_PATTERNS = [
18
+ /^config\/[^/]+\.json$/,
19
+ /^src\/app(\/|$)/,
20
+ /^src\/assets(\/|$)/,
21
+ ];
22
+
23
+ // Like FULLY_EXCLUDED_PATTERNS, but only for files that already existed before this
24
+ // update — a brand new file introduced by a newer template version should still be added.
25
+ const ADD_ONLY_PATTERNS = [
26
+ /^src\/styles(\/|$)/,
27
+ ];
28
+
29
+ function toPosixPath(relPath) {
30
+ return relPath.split(path.sep).join('/');
31
+ }
32
+
33
+ function excludeUserOwnedPaths(oldTree, newTree) {
34
+ for (const relPath of new Set([...oldTree.keys(), ...newTree.keys()])) {
35
+ const posixPath = toPosixPath(relPath);
36
+ const fullyExcluded = FULLY_EXCLUDED_PATTERNS.some((re) => re.test(posixPath));
37
+ const preexistingAddOnly = ADD_ONLY_PATTERNS.some((re) => re.test(posixPath)) && oldTree.has(relPath);
38
+
39
+ if (fullyExcluded || preexistingAddOnly) {
40
+ oldTree.delete(relPath);
41
+ newTree.delete(relPath);
42
+ }
43
+ }
44
+ }
45
+
46
+ export class TemplateDiffer {
47
+ constructor() {
48
+ this.fileGenerator = new FileGenerator();
49
+ }
50
+
51
+ async diff(projectPath, oldTemplatesRoot, oldContext, newContext) {
52
+ const newTemplatesRoot = this.fileGenerator.getTemplatesRoot();
53
+
54
+ const [oldTree, newTree] = await Promise.all([
55
+ this.fileGenerator.collectTemplateTree(oldTemplatesRoot, oldContext),
56
+ this.fileGenerator.collectTemplateTree(newTemplatesRoot, newContext),
57
+ ]);
58
+
59
+ excludeUserOwnedPaths(oldTree, newTree);
60
+
61
+ const toUpdate = [];
62
+ const toAdd = [];
63
+ const conflicts = [];
64
+ const unknown = [];
65
+ const removedByUser = [];
66
+ const noLongerGenerated = [];
67
+
68
+ for (const relPath of new Set([...oldTree.keys(), ...newTree.keys()])) {
69
+ const oldEntry = oldTree.get(relPath);
70
+ const newEntry = newTree.get(relPath);
71
+
72
+ if (oldEntry && newEntry && contentEquals(oldEntry.content, newEntry.content)) {
73
+ continue;
74
+ }
75
+
76
+ const outputPath = path.join(projectPath, relPath);
77
+ const existsOnDisk = await fs.pathExists(outputPath);
78
+
79
+ if (!oldEntry) {
80
+ // Brand new file introduced in the new version — nothing to conflict with,
81
+ // unless something else already occupies that path
82
+ if (!existsOnDisk) {
83
+ toAdd.push({ relPath, newEntry });
84
+ continue;
85
+ }
86
+
87
+ const actualContent = newEntry.isBinary
88
+ ? await fs.readFile(outputPath)
89
+ : await fs.readFile(outputPath, 'utf8');
90
+ if (!contentEquals(actualContent, newEntry.content)) {
91
+ unknown.push({ relPath });
92
+ }
93
+ continue;
94
+ }
95
+
96
+ if (!newEntry) {
97
+ // Nutin no longer generates this file; leave it alone either way,
98
+ // but warn if it's still on disk since it may now be stale/broken.
99
+ if (existsOnDisk) {
100
+ noLongerGenerated.push({ relPath });
101
+ }
102
+ continue;
103
+ }
104
+
105
+ if (!existsOnDisk) {
106
+ removedByUser.push({ relPath });
107
+ continue;
108
+ }
109
+
110
+ const actualContent = oldEntry.isBinary
111
+ ? await fs.readFile(outputPath)
112
+ : await fs.readFile(outputPath, 'utf8');
113
+
114
+ if (contentEquals(actualContent, oldEntry.content)) {
115
+ toUpdate.push({ relPath, newEntry });
116
+ } else if (!contentEquals(actualContent, newEntry.content)) {
117
+ // Differs from both baselines — a real conflict.
118
+ conflicts.push({ relPath, actualContent, newEntry });
119
+ }
120
+ }
121
+
122
+ return { toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated };
123
+ }
124
+ }
@@ -0,0 +1,25 @@
1
+ import path from 'path';
2
+ import * as fsExtra from 'fs-extra';
3
+ import { getCiCommand, detectPackageManager } from '../common/package-json-helper.mjs';
4
+ import { PACKAGE_VERSION as packageVersion } from '../common/package-data.mjs';
5
+
6
+ const fs = fsExtra.default;
7
+
8
+ export class UpdateContextBuilder {
9
+ async buildContexts(projectPath, meta) {
10
+ const packageJson = await fs.readJSON(path.join(projectPath, 'package.json'));
11
+ const packageManager = meta.packageManager ?? (await detectPackageManager(projectPath));
12
+
13
+ const baseContext = {
14
+ projectName: packageJson.name,
15
+ packageManager,
16
+ ...meta.features,
17
+ ciCommand: getCiCommand(packageManager),
18
+ };
19
+
20
+ return {
21
+ oldContext: { ...baseContext, version: meta.version },
22
+ newContext: { ...baseContext, version: packageVersion },
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,22 @@
1
+ import { print } from '../utils/print.mjs';
2
+
3
+ export function printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated }) {
4
+ print.section('\nSummary:');
5
+ print.info(` ${toUpdate.length} file(s) will be updated (untouched since generation)`);
6
+ print.info(` ${toAdd.length} new file(s) will be added`);
7
+ if (conflicts.length > 0) {
8
+ print.boldError(` ${conflicts.length} file(s) you modified need manual attention`);
9
+ }
10
+ if (noLongerGenerated.length > 0) {
11
+ print.boldError(` ${noLongerGenerated.length} file(s) nutin no longer generates — left in place, review manually:`);
12
+ noLongerGenerated.forEach(({ relPath }) => print.section(` - ${relPath}`));
13
+ }
14
+ if (unknown.length > 0) {
15
+ print.section(` ${unknown.length} file(s) have no baseline to compare against — left untouched:`);
16
+ unknown.forEach(({ relPath }) => print.section(` - ${relPath}`));
17
+ }
18
+ if (removedByUser.length > 0) {
19
+ print.section(` ${removedByUser.length} file(s) nutin used to generate but you removed — not recreated:`);
20
+ removedByUser.forEach(({ relPath }) => print.section(` - ${relPath}`));
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ export function parseVersion(version) {
2
+ const [major, minor, patch] = version.split('.').map((n) => parseInt(n, 10) || 0);
3
+ return { major, minor, patch };
4
+ }
5
+
6
+ export function compareVersions(a, b) {
7
+ const va = parseVersion(a);
8
+ const vb = parseVersion(b);
9
+ if (va.major !== vb.major) return va.major - vb.major;
10
+ if (va.minor !== vb.minor) return va.minor - vb.minor;
11
+ return va.patch - vb.patch;
12
+ }
@@ -0,0 +1,36 @@
1
+ export const chalk = {
2
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
3
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
4
+ blue: (s) => `\x1b[34m${s}\x1b[0m`,
5
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
6
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
7
+ purple: (s) => `\x1b[35m${s}\x1b[0m`,
8
+ gray: (s) => `\x1b[90m${s}\x1b[0m`,
9
+ boldGreen: (s) => `\x1b[1;32m${s}\x1b[0m`,
10
+ boldRed: (s) => `\x1b[1;31m${s}\x1b[0m`,
11
+ boldBlue: (s) => `\x1b[1;34m${s}\x1b[0m`,
12
+ boldYellow: (s) => `\x1b[1;33m${s}\x1b[0m`,
13
+ boldCyan: (s) => `\x1b[1;36m${s}\x1b[0m`,
14
+ boldPurple: (s) => `\x1b[1;35m${s}\x1b[0m`,
15
+ boldGray: (s) => `\x1b[1;90m${s}\x1b[0m`,
16
+ };
17
+
18
+ export const print = {
19
+ head: (s) => console.info(`${chalk.purple(s)}`),
20
+ boldHead : (s) => console.info(`${chalk.boldPurple(s)}`),
21
+ section: (s) => console.info(`${chalk.yellow(s)}`),
22
+ boldSection: (s) => console.info(`${chalk.boldYellow(s)}`),
23
+ info: (s) => console.info(`${chalk.green(s)}`),
24
+ boldInfo: (s) => console.info(`${chalk.boldGreen(s)}`),
25
+ success: (s) => console.log(`${chalk.cyan(s)}`),
26
+ boldSuccess: (s) => console.log(`${chalk.boldCyan(s)}`),
27
+ gray: (s) => console.info(`${chalk.gray(s)}`),
28
+ boldGray: (s) => console.info(`${chalk.boldGray(s)}`),
29
+ blue: (s) => console.info(`${chalk.blue(s)}`),
30
+ boldBlue: (s) => console.info(`${chalk.boldBlue(s)}`),
31
+ error: (s) => console.error(`${chalk.red(s)}`),
32
+ boldError: (s) => console.error(`${chalk.boldRed(s)}`),
33
+ grayError: (s) => console.error(`${chalk.boldGray(s)}`),
34
+ warn: (s) => console.warn(`${chalk.yellow(s)}`),
35
+ debug: (s) => console.debug(`${chalk.gray(s)}`),
36
+ }
@@ -0,0 +1,4 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ export const promiseExec = promisify(exec);
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@nutin/cli",
3
- "version": "1.3.0",
4
- "description": "Build SPAs by understanding SPAs. A minimal frontend toolkit crafted for learning and lean applications.",
3
+ "version": "2.0.0",
4
+ "description": "The structure of a framework. The freedom of vanilla.",
5
5
  "preferGlobal": true,
6
6
  "main": "lib/index.mjs",
7
7
  "bin": {
8
+ "cli": "./bin/cli.mjs",
9
+ "nutin": "./bin/cli.mjs",
8
10
  "create-nutin-app": "./bin/create-app.mjs",
9
- "nutin-new": "./bin/create-app.mjs"
11
+ "nutin-new": "./bin/create-app.mjs",
12
+ "nutin-add": "./bin/add-feature.mjs",
13
+ "nutin-update": "./bin/update-app.mjs"
10
14
  },
11
15
  "scripts": {
12
16
  "patch": "npm version patch -m 'CI/CD: Bump version to %s'",
@@ -16,24 +20,25 @@
16
20
  "publish": "npm publish --access public"
17
21
  },
18
22
  "keywords": [
19
- "nutin",
20
- "Frontend",
21
- "SPA",
22
- "Learning",
23
- "Minimalist",
24
- "Lighweight",
25
- "Customizable"
23
+ "Nutin",
24
+ "Vanilla",
25
+ "Framework",
26
+ "Freedom",
27
+ "Structure",
28
+ "Ownership"
26
29
  ],
27
30
  "author": "Loïs Kouninef <loiskouninef@gmail.com>",
28
31
  "license": "MIT",
29
32
  "dependencies": {
30
33
  "commander": "^9.0.0",
34
+ "diff": "^9.0.0",
31
35
  "fs-extra": "^11.0.0",
32
36
  "handlebars": "^4.7.8",
33
- "inquirer": "^9.0.0"
37
+ "inquirer": "^9.0.0",
38
+ "tar": "^7.5.22"
34
39
  },
35
40
  "engines": {
36
- "node": ">=18"
41
+ "node": ">=22"
37
42
  },
38
43
  "repository": {
39
44
  "type": "git",
@@ -7,6 +7,11 @@ pnpm-debug.log*
7
7
 
8
8
  /dist/
9
9
  /dist-build/
10
+ /coverage/
11
+
12
+ # Generated by "validate-docker" (run via docker:build) from tools/docker/*.template — only present when the docker feature is added
13
+ /tools/docker/Dockerfile
14
+ /tools/docker/nginx.conf
10
15
 
11
16
  .DS_Store
12
17
  .DS_Store?
@@ -40,4 +45,4 @@ Desktop.ini
40
45
  .vscode-test
41
46
 
42
47
  tmp/
43
- temp/
48
+ temp/
@@ -0,0 +1,169 @@
1
+ # Working on this project
2
+
3
+ This project was generated with Nutin {{version}}.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ src/
9
+ app/ application code (components, views, services)
10
+ core/ Nutin framework internals — see "Nutin" section before editing
11
+ styles/ project-level Sass
12
+ config/ i18n (languages.json) and SEO configuration
13
+ tools/ builder, dev server, generator, testin-nutin runner, docker (optional)
14
+ nutin.config.js build/feature config — see below
15
+ .nutin-meta.json Nutin version, package manager, enabled features (source of truth for what's installed)
16
+ ```
17
+
18
+ ## Conventions
19
+
20
+ - Favor small, composable components.
21
+ - One `View` = one route.
22
+ - Import everything from barrel files (`core/index.js`, `components/index.js`, `views/index.js`, `services/index.js`) — do not deep-import.
23
+
24
+ ## Commands
25
+
26
+ - Serve: `{{packageManager}} run serve` (build + serve), `{{packageManager}} run serve:only` (no rebuild), `{{packageManager}} run dev` (watch mode).
27
+ - Build: `{{packageManager}} run build` (dev) or `{{packageManager}} run build:prod` (production).
28
+ - Generate: `{{packageManager}} run generate ELEMENT ELEMENT_NAME` (`ELEMENT` = `component`/`view`/`service`, supports nested paths, updates the nearest `index.ts` barrel).
29
+ - Test: `{{packageManager}} run testin-nutin` (build + run), `{{packageManager}} run testin-nutin:only` (no rebuild), `{{packageManager}} run testin-nutin:watch`.
30
+
31
+ ## Base classes
32
+
33
+ ```
34
+ Service<T> — app-wide singleton (router, event bus, i18n, http client, pipe registry, app-specific services)
35
+ BaseComponent<T> — render/destroy engine, data-* parsing, children
36
+ ├── Component — reusable, non-routed UI; config/props + data-bind
37
+ └── View — one per route; mounted/unmounted by the Router
38
+ ```
39
+
40
+ - `render()`: `onBeforeRender()` → sanitize + set `innerHTML` → mount children (`data-component`/`data-catalog`) → parse `data-i18n`/`data-pipe` → bind `data-event` listeners → `onAfterRender()`. Runs in full on every `render()` call — no virtual DOM/diffing.
41
+ - `destroy()`: unbinds events, unsubscribes bus listeners, destroys children, removes the element.
42
+ - `Service`: direct `new` throws — always use `MyService.getInstance()` (or the exported const). Auto-binds all methods to the instance. `registerCleanup(fn)` for teardown; `Service.destroyAll()` runs on `beforeunload`.
43
+ - Overridable hooks (all no-op by default): `onBeforeRender`, `onAfterRender`, `onBeforeDestroy`, `onAfterDestroy`, `generateTemplate()`, `registerChildren()`. `View` adds router-only `onEnter()`/`onExit()`.
44
+
45
+ ## Children components
46
+
47
+ - Override `registerChildren(): ComponentConfig[]` to declare children — matched against `[data-component="selector"]` elements in the rendered template. ComponentConfig: `{ selector: 'my-selector', factory: (el) => new MyComponent(el, {...} )}`.
48
+ - For repeated lists, use the inherited `createCatalogComponents({ items, elementName, selector, component })` inside `registerChildren()` instead of hand-writing one entry per item — targets `[data-catalog="selector"]`.
49
+
50
+ ## `data-*` attributes
51
+
52
+ | Attribute | Purpose |
53
+ |---|---|
54
+ | `data-event="event:handler[:args]"` | Declarative event binding, see below. |
55
+ | `data-i18n="key"` | Sets translated text/placeholder via `I18nService`. |
56
+ | `data-pipe="name[:args]\|name2..."` | Pipes value/text through registered pipes, chainable with `\|`. |
57
+ | `data-pipe-source="..."` | Overrides the raw value fed into `data-pipe`. |
58
+ | `data-optional` | Removes the element post-render if it ends up empty (tag-specific check: `src`/`value`/`textContent`). |
59
+ | `data-component="selector"` | Child component mount point, from `registerChildren()`. |
60
+ | `data-catalog="selector"` | Repeated-children mount point, from `createCatalogComponents()`. |
61
+ | `data-bind="propKey"` | `Component`-only two-way field binding; read back with `getValues()`. |
62
+
63
+ ## DOM events binding
64
+
65
+ Handlers are (re-)bound on every `render()`, not at build time. Always use private methods prefixed with underscore:
66
+
67
+ ```html
68
+ <div data-event="event:_myFunction:@value,@id">
69
+ ```
70
+ ```ts
71
+ private _myFunction(value: string, id: string) { doStuff() }
72
+ ```
73
+
74
+ Tokens: `@value`, `@id`, `@class`, `@textContent`, `@event`, `@target`, `@attr:name`, `@dataset:key`, plus string/number literals.
75
+
76
+ ## Core services
77
+
78
+ - `AppEventBus` — `subscribe`/`once`/`emit`/`off`. Inside a component, prefer `this.listen(event, cb)` (auto-unsubscribes on `destroy()`) or `this.listenToRenderEvents([event1, event2, ...])` (re-runs `this.render()` whenever any of the given events fire) over calling the bus directly. Typed facades — each `on*` subscriber returns a closure you call directly to unsubscribe (`const off = Navigation.onNavigate(cb); ...; off();`), no need to keep the original callback reference around for `off()`:
79
+ - `Navigation` — `navigateTo(path)`/`onNavigate`, `reload()`/`onReload`.
80
+ - `Lifecycle` — `viewMount(viewName)`/`onViewMount`, `viewUnmount(viewName)`/`onViewUnmount`, plus generic `beforeRender`/`afterRender`/`beforeDestroy`/`afterDestroy`.
81
+ - `AppRouter(routes)` — call once in `main.ts`; routes support `:param`/`:param?` and per-route guards.
82
+ - `registerGlobals({ before?, after? })` — each entry is `{ component, id }` (a bare class, constructed internally); mounts them outside `<main id="app">` (`before`/`after` control placement relative to it), stamping each root element with `id`. `hideGlobals(ids)`/`revealGlobals(ids)` (same module) toggle visibility by those ids from any component or view. Call once in `main.ts`.
83
+ - `AppHttpClient` — `get/post/put/patch/delete<T>(endpoint, data?, config?)`; non-OK responses throw `HttpError`.
84
+ - `I18nService` — `translate(key, fallback?)`, `setCurrentLanguage(lang)`; language list comes from `config/languages.json`.
85
+ - `AppPipeRegistry` — `register(name, fn)` / `apply(name, value, args)`; backs `data-pipe`.
86
+
87
+ ## Styling
88
+
89
+ - Sass is the primary styling system; Tailwind is optional (`nutin.config.js`'s `tailwind` flag).
90
+ - Naming convention: `.my-element__my-class`.
91
+
92
+ ## Pipes
93
+
94
+ ```
95
+ // currency
96
+ AppPipeRegistry.register('currency');
97
+
98
+ // date
99
+ AppPipeRegistry.register('date');
100
+
101
+ // number (toFixed)
102
+ AppPipeRegistry.register('number');
103
+
104
+ // string pipes
105
+ AppPipeRegistry.register('uppercase');
106
+ AppPipeRegistry.register('lowercase');
107
+ AppPipeRegistry.register('capitalize');
108
+ AppPipeRegistry.register('capitalizeAll');
109
+ AppPipeRegistry.register('truncate');
110
+
111
+ // utility pipes
112
+ AppPipeRegistry.register('default');
113
+ AppPipeRegistry.register('json');
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ ### `nutin.config.js`
119
+
120
+ Central options/tools config (see the inline comments for details):
121
+ - `i18n`, `tailwind`, `generateSEO`, `dockerPorts` — top-level feature toggles (`dockerPorts` only applies once the docker feature is added).
122
+ - `generator.{generateStylesheet, generateLocales, generateTest}` — what the generator scaffolds alongside new elements.
123
+ - `builder.{sass.paths, esbuild}` — build pipeline behavior.
124
+ - `testinNutin.{includeFramework, includeTools, includeApp, jsdomOptions}` — test discovery/environment.
125
+
126
+ ### `config/seo.json`
127
+
128
+ Consumed by builder (production only) to generate static HTML per route. Unused if `nutin.config.js -> generateSEO` is disabled.
129
+
130
+ ### `config/languages.json`
131
+
132
+ Consumed by i18nService and builder. Unused if i18n is disabled.
133
+
134
+ ## Security
135
+
136
+ `SecurityHelper.sanitizeTemplate()` runs on every `render()` before `innerHTML` is set — `trustLevel` (`'strict' | 'normal' | 'trusted'`, default `'normal'`) controls how much is stripped (`<script>`/inline handlers always; `<iframe>`/`javascript:`/`data:` under `strict` only). Use `SecurityHelper.escapeHtml()` when manually interpolating strings into templates.
137
+
138
+ ## Testing (testin-nutin)
139
+
140
+ Nutin's own hand-built test runner + jsdom - not Jest. `describe`/`it` only (**no `test()` alias**), `beforeAll`/`beforeEach`/`afterEach`/`afterAll` (one of each per suite, no stacking - inside `describe` statement, not top-level). All globals — no imports needed in test files.
141
+
142
+ - Matchers: `toBe`, `toEqual`, `toContain`, `toBeTruthy`/`toBeFalsy`, `toBeDefined`/`toBeUndefined`, `toBeInstanceOf`, `toHaveBeenCalled`/`toHaveBeenCalledWith`, `toBeLessThan`/`toBeGreaterThan`, `toThrow` — each has a `.not` counterpart.
143
+ - `spyOn(obj, 'method')` returns `.calls`/`.callCount`/`.lastCall`, `.andCallFake(fn)`/`.andReturn(v)`, `.restore()` — unlike mocks, calls pass through by default.
144
+ - `tools/testin-nutin/mocks/` has ready-made `MockEventBus`/`MockHttpClient`/`MockI18n`/`MockRouter` for constructor injection.
145
+ - `useFakeTimers()`/`useRealTimers()`/`advanceTimersByTime(ms)`/`runAllTimers()` for time-dependent code.
146
+ - Test discovery/scope controlled by `nutin.config.js`'s `testinNutin.includeApp`/`includeFramework`.
147
+
148
+ ## Nutin
149
+
150
+ Nutin provides the project tooling, build pipeline and generated structure. Its source code lives in `src/core/`.
151
+
152
+ Tests are an important source of documentation. They describe expected behavior and should be consulted before modifying existing functionality.
153
+ Before changing existing functionality, inspect the corresponding tests.
154
+ When adding behavior, add or update tests.
155
+
156
+ When modifying Nutin `core/` files:
157
+
158
+ 1. Explore the alternatives: is modifying Nutin core files the best approach long-term? Always ask for validation.
159
+ 2. Check the corresponding Nutin documentation and tests.
160
+ 3. Preserve existing conventions.
161
+ 4. Inspect existing implementations before creating new abstractions.
162
+ 5. Reuse existing utilities/helpers where appropriate.
163
+ 6. Do not rewrite generated configuration unnecessarily.
164
+ 7. Keep changes focused on the requested feature.
165
+ 8. Run tests.
166
+
167
+ ## Documentation
168
+
169
+ [Nutin Documentation](https://github.com/LoisKOUNINEF/nutin/tree/main/docs)