@nutin/cli 1.3.1 → 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 (287) hide show
  1. package/CHANGELOG.md +89 -9
  2. package/README.md +20 -35
  3. package/bin/add-feature.mjs +9 -0
  4. package/bin/cli.mjs +8 -0
  5. package/bin/create-app.mjs +5 -4
  6. package/bin/update-app.mjs +9 -0
  7. package/lib/index.mjs +76 -25
  8. package/lib/src/add-feature/feature-adder.mjs +83 -0
  9. package/lib/src/add-feature/feature-context-builder.mjs +25 -0
  10. package/lib/src/add-feature/nutin-config-updater.mjs +29 -0
  11. package/lib/src/add-feature/package-json-updater.mjs +49 -0
  12. package/lib/src/common/feature-registry.mjs +11 -0
  13. package/lib/src/common/file-generator.mjs +159 -0
  14. package/lib/src/common/meta-bootstrap-prompt.mjs +33 -0
  15. package/lib/src/common/package-data.mjs +10 -0
  16. package/lib/src/common/package-json-helper.mjs +103 -0
  17. package/lib/src/common/project-meta.mjs +54 -0
  18. package/lib/src/create-app/context-builder.mjs +21 -0
  19. package/lib/src/{git-manager.mjs → create-app/git-manager.mjs} +2 -2
  20. package/lib/src/create-app/json-manager.mjs +73 -0
  21. package/lib/src/{prompts.mjs → create-app/new-app-prompt.mjs} +6 -6
  22. package/lib/src/{project-generator.mjs → create-app/project-generator.mjs} +10 -8
  23. package/lib/src/create-app/success-message.mjs +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 -19
  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 +68 -35
  53. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +124 -0
  54. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +9 -8
  55. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +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 +7 -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 +2 -2
  61. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +62 -0
  62. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +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 +24 -33
  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/data-binding.helper.test.js.hbs +75 -0
  70. package/templates/base/src/core/base-classes/service/service.test.js.hbs +150 -0
  71. package/templates/base/src/core/base-classes/service/service.ts.hbs +26 -20
  72. package/templates/base/src/core/base-classes/view/view.test.js.hbs +74 -0
  73. package/templates/base/src/core/base-classes/view/view.ts.hbs +17 -41
  74. package/templates/base/src/core/config.ts.hbs +7 -0
  75. package/templates/base/src/core/index.ts.hbs +0 -3
  76. package/templates/base/src/core/internals.d.ts.hbs +27 -0
  77. package/templates/base/src/core/nutin-config.d.ts.hbs +4 -0
  78. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +67 -3
  79. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +9 -4
  80. package/templates/base/src/core/services/event-bus/index.ts.hbs +3 -0
  81. package/templates/base/src/core/services/event-bus/lifecycle.facade.test.js.hbs +123 -0
  82. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +61 -0
  83. package/templates/base/src/core/services/event-bus/navigation.facade.test.js.hbs +46 -0
  84. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +25 -0
  85. package/templates/base/src/core/services/globals/globals.test.js.hbs +149 -0
  86. package/templates/base/src/core/services/globals/globals.ts.hbs +95 -0
  87. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +25 -0
  88. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +20 -0
  89. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +43 -0
  90. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  91. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +22 -4
  92. package/templates/base/src/core/services/index.ts.hbs +5 -6
  93. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  94. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  95. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  96. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  97. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +149 -6
  98. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +40 -29
  99. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  100. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  101. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +14 -38
  102. package/templates/base/src/core/services/router/router.test.js.hbs +226 -2
  103. package/templates/base/src/core/services/router/router.ts.hbs +40 -8
  104. package/templates/base/src/index.html.hbs +2 -6
  105. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  106. package/templates/base/src/styles/base/_variables.scss.hbs +1 -86
  107. package/templates/base/src/styles/main.scss.hbs +1 -18
  108. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  109. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  110. package/templates/base/tools/builder/builder.js.hbs +24 -26
  111. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  112. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  113. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  114. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  115. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  116. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  117. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  118. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  119. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  120. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  121. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  122. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  123. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  124. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  125. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  126. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  127. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  128. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  129. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  130. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  131. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  132. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  133. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  134. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  136. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  137. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  138. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  139. package/templates/base/tools/dev/serve.js.hbs +11 -1
  140. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  141. package/templates/base/tools/generator/generator.js.hbs +64 -47
  142. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  143. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  144. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  145. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  146. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  147. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  148. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  149. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +4 -0
  150. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +7 -0
  151. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  152. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  153. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  154. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  155. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  156. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  157. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +3 -2
  158. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  159. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  160. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  164. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  165. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  166. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  167. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  168. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  169. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  170. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  171. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  172. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  173. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  174. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  175. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  176. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  177. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  178. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  179. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  180. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  181. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  182. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  183. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  185. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  186. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  187. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  188. package/templates/base/tools/utils/format.js.hbs +10 -10
  189. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  190. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  191. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  192. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  193. package/templates/base/tools/utils/index.js.hbs +1 -3
  194. package/templates/base/tools/utils/languages.js.hbs +15 -0
  195. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  196. package/templates/base/tools/utils/print.js.hbs +19 -13
  197. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  198. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  199. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  200. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  201. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  202. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  203. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  204. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  205. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  206. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  207. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  208. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  209. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  211. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  212. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  213. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  214. package/lib/src/context-builder.mjs +0 -40
  215. package/lib/src/file-generator.mjs +0 -109
  216. package/lib/src/json-generator.mjs +0 -31
  217. package/lib/src/package-manager.mjs +0 -139
  218. package/lib/src/print.mjs +0 -32
  219. package/lib/src/utils.mjs +0 -18
  220. package/lib/src/version.mjs +0 -1
  221. package/templates/base/builder.config.js.hbs +0 -36
  222. package/templates/base/src/app/components/common/anchor/anchor.component.ts.hbs +0 -32
  223. package/templates/base/src/app/components/common/button/button.component.ts.hbs +0 -40
  224. package/templates/base/src/app/dom.test.js.hbs +0 -13
  225. package/templates/base/src/app/services/example/example.service.ts.hbs +0 -18
  226. package/templates/base/src/app/views/home/home.view.test.js.hbs +0 -26
  227. package/templates/base/src/core/services/store/store.test.js.hbs +0 -8
  228. package/templates/base/src/core/services/store/store.ts.hbs +0 -39
  229. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.test.js.hbs +0 -0
  230. package/templates/base/src/core/utils/anchor-manager/anchor-manager.utils.ts.hbs +0 -113
  231. package/templates/base/src/core/utils/button-manager/button-manager.utils.test.js.hbs +0 -249
  232. package/templates/base/src/core/utils/button-manager/button-manager.utils.ts.hbs +0 -131
  233. package/templates/base/src/core/utils/helpers/attributes.helper.ts.hbs +0 -50
  234. package/templates/base/src/core/utils/index.ts.hbs +0 -2
  235. package/templates/base/src/libs/index.ts.hbs +0 -4
  236. package/templates/base/src/libs/popover/helpers/focus-trap.helper.ts.hbs +0 -197
  237. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +0 -75
  238. package/templates/base/src/libs/popover/popover.ts.hbs +0 -138
  239. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -75
  240. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  241. package/templates/base/src/styles/core/_anchors.scss.hbs +0 -10
  242. package/templates/base/src/styles/core/_index.scss.hbs +0 -6
  243. package/templates/base/src/styles/core/_popover.scss.hbs +0 -96
  244. package/templates/base/src/styles/core/_snackbar.scss.hbs +0 -50
  245. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  246. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  247. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  248. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  249. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  250. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  251. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  252. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  253. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  254. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  255. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  256. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  257. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  258. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  259. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  260. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  261. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  262. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  263. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -254
  264. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  265. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  266. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  267. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  268. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  269. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  270. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  271. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  272. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  273. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  274. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  275. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  276. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  277. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  278. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  279. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  280. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  281. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  282. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  283. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  284. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  285. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  286. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  287. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -0,0 +1,152 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getFilesRecursive, print, pascalCased, capitalized } from '../../../utils/index.js';
4
+ import { PATHS } from '../app/paths.js';
5
+ import { minifyHTML } from './minify-html.js';
6
+
7
+ const PLACEHOLDER = '__TEMPLATE_PLACEHOLDER__';
8
+ const TARGET_EXTENSIONS = ['.ts', '.js'];
9
+ const TEMPLATE_REGEX = /const\s+(?:template|templateFn)\s*=?\s*(?:\(.*?\)\s*=>\s*)?`([\s\S]*?)`/;
10
+
11
+ // A component/view's template can live inline in its .ts/.js file or in a
12
+ // colocated .html sibling merged in via `__TEMPLATE_PLACEHOLDER__` — resolved
13
+ // automatically per file rather than through a global config flag:
14
+ // external + placeholder -> merge external in
15
+ // external + real inline markup -> conflict, fail
16
+ // no external + real inline markup -> minify inline
17
+ // no external + still the placeholder -> nothing to use, fail
18
+ export async function resolveTemplates() {
19
+ const htmlFiles = await getFilesRecursive(PATHS.tempApp, 'html');
20
+ const resolvedPaths = new Set();
21
+ const failures = [];
22
+
23
+ for (const htmlPath of htmlFiles) {
24
+ await resolveExternalTemplate(htmlPath, resolvedPaths, failures);
25
+ }
26
+
27
+ await resolveRemainingTemplates(resolvedPaths, failures);
28
+
29
+ if (failures.length) {
30
+ throw new Error(`Failed to resolve ${failures.length} template(s).`);
31
+ }
32
+ }
33
+
34
+ // Normally a view/component has exactly one sibling here — `.js` in dev (tsc
35
+ // output), `.ts` in prod (esbuild bundles straight from source, tsc runs
36
+ // --noEmit there). Checking whichever exist(s) rather than assuming one, so
37
+ // this can't silently regress again if that dev/prod split ever changes.
38
+ async function resolveExternalTemplate(htmlPath, resolvedPaths, failures) {
39
+ const htmlFilename = path.basename(htmlPath);
40
+
41
+ let htmlContent;
42
+ try {
43
+ htmlContent = await fs.readFile(htmlPath, 'utf-8');
44
+ } catch (err) {
45
+ print.warn(`Failed to read "${htmlFilename}". ${err.message}`);
46
+ failures.push(htmlFilename);
47
+ return;
48
+ }
49
+
50
+ const minifiedHtml = await minifyHTML(htmlContent);
51
+ let mergedCount = 0;
52
+ let conflictCount = 0;
53
+
54
+ for (const extension of TARGET_EXTENSIONS) {
55
+ const codePath = htmlPath.replace(/\.html$/, extension);
56
+
57
+ let content;
58
+ try {
59
+ content = await fs.readFile(codePath, 'utf-8');
60
+ } catch (err) {
61
+ if (err.code === 'ENOENT') continue;
62
+ print.warn(`Cannot update "${path.basename(codePath)}". ${err.message}`);
63
+ failures.push(path.basename(codePath));
64
+ resolvedPaths.add(codePath);
65
+ continue;
66
+ }
67
+
68
+ resolvedPaths.add(codePath);
69
+
70
+ if (content.includes(PLACEHOLDER)) {
71
+ await fs.writeFile(codePath, content.replace(PLACEHOLDER, minifiedHtml));
72
+ mergedCount++;
73
+ continue;
74
+ }
75
+
76
+ print.warn(describeConflict(codePath));
77
+ failures.push(path.basename(codePath));
78
+ conflictCount++;
79
+ }
80
+
81
+ if (mergedCount > 0) {
82
+ await fs.unlink(htmlPath);
83
+ } else if (mergedCount + conflictCount === 0) {
84
+ print.warn(describeOrphanHtml(htmlPath));
85
+ failures.push(htmlFilename);
86
+ }
87
+ }
88
+
89
+ async function resolveRemainingTemplates(resolvedPaths, failures) {
90
+ const codeFiles = await getFilesRecursive(PATHS.tempApp, TARGET_EXTENSIONS);
91
+
92
+ for (const codePath of codeFiles) {
93
+ if (resolvedPaths.has(codePath)) continue;
94
+
95
+ const content = await fs.readFile(codePath, 'utf-8');
96
+ const match = content.match(TEMPLATE_REGEX);
97
+ if (!match) continue;
98
+
99
+ const [full, body] = match;
100
+
101
+ if (body === PLACEHOLDER) {
102
+ print.warn(describeMissingTemplate(codePath));
103
+ failures.push(path.basename(codePath));
104
+ continue;
105
+ }
106
+
107
+ const minified = await minifyHTML(body);
108
+ await fs.writeFile(codePath, content.replace(full, full.replace(body, minified)));
109
+ }
110
+ }
111
+
112
+ function describeCodeFile(codePath) {
113
+ const filename = path.basename(codePath);
114
+ const basename = filename.replace(/\.(ts|js)$/, '');
115
+ const lastDot = basename.lastIndexOf('.');
116
+ const namePart = lastDot === -1 ? basename : basename.slice(0, lastDot);
117
+ const suffix = lastDot === -1 ? '' : basename.slice(lastDot + 1);
118
+ const kind = suffix ? capitalized(suffix) : 'Component';
119
+ const className = `${pascalCased(namePart)}${kind}`;
120
+
121
+ return { filename, basename, kind, className };
122
+ }
123
+
124
+ function describeConflict(codePath) {
125
+ const { filename, basename, kind, className } = describeCodeFile(codePath);
126
+
127
+ return `${kind} "${className}" has both an inline template and an external template.
128
+
129
+ Use either:
130
+ ${filename}
131
+ or:
132
+ ${filename} (template placeholder) + ${basename}.html
133
+ Remove one of the template definitions.`;
134
+ }
135
+
136
+ function describeMissingTemplate(codePath) {
137
+ const { filename, basename, kind, className } = describeCodeFile(codePath);
138
+
139
+ return `${kind} "${className}" has a template placeholder but no external template.
140
+
141
+ Expected:
142
+ ${filename} (template placeholder) + ${basename}.html
143
+ Add ${basename}.html, or replace the placeholder in ${filename} with inline markup.`;
144
+ }
145
+
146
+ function describeOrphanHtml(htmlPath) {
147
+ const htmlFilename = path.basename(htmlPath);
148
+ const basename = htmlFilename.replace(/\.html$/, '');
149
+
150
+ return `"${htmlFilename}" has no matching component or view (expected ${basename}.ts or ${basename}.js).
151
+ Remove ${htmlFilename}, or add ${basename}.ts with a template placeholder to merge it into.`;
152
+ }
@@ -0,0 +1,15 @@
1
+ import { minify } from 'html-minifier-terser';
2
+
3
+ export async function minifyHTML(html) {
4
+ return minify(html, {
5
+ collapseWhitespace: true,
6
+ removeComments: true,
7
+ removeRedundantAttributes: true,
8
+ removeScriptTypeAttributes: true,
9
+ removeStyleLinkTypeAttributes: true,
10
+ useShortDoctype: true,
11
+ keepClosingSlash: true,
12
+ minifyCSS: true,
13
+ minifyJS: true,
14
+ });
15
+ }
@@ -0,0 +1,8 @@
1
+ import { errorExit } from '../../../utils/index.js';
2
+ import { resolveTemplates } from './merge-templates.js';
3
+
4
+ try {
5
+ await resolveTemplates();
6
+ } catch (err) {
7
+ errorExit(err);
8
+ }
@@ -1,8 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { exit } from 'process';
4
- import { getFilesRecursive, print, isVerbose } from '../../utils/index.js';
5
- import { PATHS } from './paths.js';
3
+ import { getFilesRecursive, print, errorExit } from '../../../utils/index.js';
4
+ import { PATHS } from '../app/paths.js';
6
5
 
7
6
  const destDir = path.join(PATHS.tempSource, 'locales');
8
7
 
@@ -50,7 +49,7 @@ async function removeJsonFiles(directory) {
50
49
  try {
51
50
  await fs.promises.access(directory);
52
51
  } catch {
53
- print.info(`Directory ${directory} does not exist, skipping cleanup`);
52
+ print.grayError(`Directory ${directory} does not exist, skipping cleanup`);
54
53
  return;
55
54
  }
56
55
 
@@ -60,19 +59,13 @@ async function removeJsonFiles(directory) {
60
59
  if (file.includes(`${path.sep}locales${path.sep}`)) {
61
60
  try {
62
61
  await fs.promises.unlink(file);
63
- if (isVerbose) print.info(`Deleted locale JSON file: ${file}`);
64
62
  } catch (err) {
65
63
  print.error(`Failed to remove ${file}: ${err.message}`);
66
64
  }
67
- }
68
- else if (isVerbose) {
69
- print.info(`Skipping non-locale JSON file: ${file}`);
70
65
  }
71
66
  }
72
67
 
73
68
  await removeEmptyDirectories(directory);
74
-
75
- if (isVerbose) print.boldInfo(`JSON files cleanup complete.`);
76
69
  }
77
70
 
78
71
  async function removeEmptyDirectories(directory) {
@@ -102,10 +95,9 @@ async function removeEmptyDirectories(directory) {
102
95
  const updatedItems = await fs.promises.readdir(directory);
103
96
  if (updatedItems.length === 0) {
104
97
  await fs.promises.rmdir(directory, { recursive: false });
105
- if (isVerbose) print.info(`📁 Removed empty directory: ${directory}`);
106
98
  }
107
99
  } catch (err) {
108
- print.error(`Failed to remove empty directory ${directory}: ${err.message}`);
100
+ print.grayError(`Failed to remove empty directory ${directory}: ${err.message}`);
109
101
  }
110
102
  }
111
103
  }
@@ -114,19 +106,16 @@ async function mergeJson() {
114
106
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
115
107
 
116
108
  const localeFilesMap = findLocaleFiles();
117
-
118
109
  for (const [locale, files] of Object.entries(localeFilesMap)) {
119
110
  const combined = combineJsonFiles(files);
120
111
  fs.writeFileSync(
121
112
  `${destDir}/${locale}.json`,
122
113
  JSON.stringify(combined, null, 2)
123
114
  );
124
- if (isVerbose) print.boldInfo(`Json files for locale ${locale} merged successfully`);
125
115
  }
126
- removeJsonFiles(PATHS.tempApp);
116
+ await removeJsonFiles(PATHS.tempApp);
127
117
  }
128
118
 
129
119
  mergeJson().catch((err) => {
130
- print.boldError(`Unexpected error: ${err.message}`);
131
- exit(1);
120
+ errorExit(err)
132
121
  });
@@ -0,0 +1,37 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { gzipSync, brotliCompressSync, constants } from 'zlib';
3
+ import { print, getFilesRecursive, errorExit } from '../../../utils/index.js';
4
+ import { PATHS } from '../app/paths.js';
5
+
6
+ function compressStaticAssets() {
7
+ const files = getFilesRecursive(
8
+ PATHS.tempSource,
9
+ ['.js', '.css', '.json', '.svg', '.ttf', '.otf', '.eot']
10
+ );
11
+
12
+ for (const file of files) {
13
+ const content = readFileSync(file);
14
+
15
+ const gzipped = gzipSync(content, {
16
+ level: 9,
17
+ memLevel: 9,
18
+ windowBits: 15
19
+ });
20
+ writeFileSync(`${file}.gz`, gzipped);
21
+
22
+ const brotlied = brotliCompressSync(content, {
23
+ params: {
24
+ [constants.BROTLI_PARAM_QUALITY]: 11,
25
+ [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_GENERIC,
26
+ [constants.BROTLI_PARAM_SIZE_HINT]: content.length
27
+ }
28
+ });
29
+ writeFileSync(`${file}.br`, brotlied);
30
+ };
31
+ }
32
+
33
+ try {
34
+ compressStaticAssets()
35
+ } catch(err) {
36
+ errorExit(err, 'compress-files');
37
+ }
@@ -0,0 +1,30 @@
1
+ import esbuild from 'esbuild';
2
+ import path from 'path';
3
+ import { print, errorExit } from '../../../utils/index.js';
4
+ import { PATHS } from '../app/paths.js';
5
+ import { builderConfig } from '../../builder.config.js';
6
+
7
+ const ENTRY_FILE = path.join(PATHS.tempApp, 'main.ts');
8
+ const OUT_FILE = path.join(PATHS.tempSource, 'bundle.js');
9
+
10
+ async function build() {
11
+ await esbuild.build({
12
+ ...builderConfig.esbuild,
13
+ platform: 'browser',
14
+ format: 'esm',
15
+ legalComments: 'none',
16
+ loader: {
17
+ '.json': 'json',
18
+ },
19
+ define: {
20
+ 'process.env.NODE_ENV': '"production"',
21
+ },
22
+ entryPoints: [ENTRY_FILE],
23
+ outfile: OUT_FILE,
24
+ keepNames: false,
25
+ });
26
+ }
27
+
28
+ build().catch((err) => {
29
+ errorExit(err, 'esbuild');
30
+ });
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import crypto from 'crypto';
4
- import { PATHS } from './paths.js';
5
- import { print, isVerbose, getFilesRecursive } from '../../utils/index.js';
4
+ import { PATHS } from '../app/paths.js';
5
+ import { print, getFilesRecursive, errorExit } from '../../../utils/index.js';
6
6
 
7
7
  function addHashToFiles(distDir) {
8
8
  const files = getFilesRecursive(distDir, ['.js', '.css']);
@@ -24,10 +24,6 @@ function addHashToFiles(distDir) {
24
24
  const newRel = path.relative(distDir, newPath).replace(/\\/g, '/');
25
25
 
26
26
  fileMap[oldRel] = newRel;
27
-
28
- if (isVerbose) {
29
- print.info(`Renamed: ${oldRel} → ${newRel}`);
30
- }
31
27
  });
32
28
 
33
29
  updateHtmlReferences(path.join(distDir, 'index.html'), fileMap);
@@ -38,15 +34,22 @@ function hashFile(filePath) {
38
34
  return crypto.createHash('sha256').update(content).digest('hex').substring(0, 8);
39
35
  }
40
36
 
37
+ function escapeRegExp(str) {
38
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ }
40
+
41
41
  function updateHtmlReferences(htmlPath, fileMap) {
42
42
  let html = fs.readFileSync(htmlPath, 'utf8');
43
-
43
+
44
44
  Object.entries(fileMap).forEach(([oldName, newName]) => {
45
- html = html.replace(new RegExp(oldName, 'g'), newName);
45
+ html = html.replace(new RegExp(escapeRegExp(oldName), 'g'), newName);
46
46
  });
47
-
47
+
48
48
  fs.writeFileSync(htmlPath, html);
49
- if (isVerbose) print.info('Updated index.html references');
50
49
  }
51
50
 
52
- addHashToFiles(PATHS.tempSource);
51
+ try {
52
+ addHashToFiles(PATHS.tempSource)
53
+ } catch(err) {
54
+ errorExit(err, 'hash-files');
55
+ }
@@ -0,0 +1,51 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PATHS } from '../app/paths.js';
4
+ import { print } from '../../../utils/index.js';
5
+
6
+ export function generateRobotsTxt() {
7
+ const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
8
+ const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
9
+ const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
10
+ const routes = seoConfig.routes ?? [];
11
+ const disallowBots = seoConfig.disallowBots ?? [];
12
+
13
+ // Paths disallowed for all bots (disallow: true)
14
+ const globalDisallows = routes
15
+ .filter(r => r.disallow === true)
16
+ .map(r => `Disallow: ${r.path}`);
17
+
18
+ // Paths disallowed per named bot: { botName -> [path, ...] }
19
+ const perBotDisallows = {};
20
+ for (const r of routes) {
21
+ if (!Array.isArray(r.disallow)) continue;
22
+ for (const bot of r.disallow) {
23
+ (perBotDisallows[bot] ??= []).push(r.path);
24
+ }
25
+ }
26
+ // Bots that are fully blocked contribute an empty-path disallow
27
+ for (const bot of disallowBots) {
28
+ perBotDisallows[bot] ??= [];
29
+ }
30
+
31
+ const lines = [
32
+ 'User-agent: *',
33
+ 'Allow: /',
34
+ ...globalDisallows,
35
+ ];
36
+
37
+ for (const [bot, paths] of Object.entries(perBotDisallows)) {
38
+ lines.push('');
39
+ lines.push(`User-agent: ${bot}`);
40
+ if (paths.length > 0) {
41
+ for (const p of paths) lines.push(`Disallow: ${p}`);
42
+ } else {
43
+ lines.push('Disallow: /');
44
+ }
45
+ }
46
+
47
+ lines.push('', `Sitemap: ${baseUrl}/sitemap.xml`);
48
+
49
+ const outputPath = path.join(PATHS.tempSource, 'robots.txt');
50
+ fs.writeFileSync(outputPath, lines.join('\n') + '\n', 'utf-8');
51
+ }
@@ -0,0 +1,16 @@
1
+ import { print, errorExit } from '../../../utils/index.js';
2
+ import { generateSeoHtml } from './generate-seo-html.js';
3
+ import { generateRobotsTxt } from './generate-robots-txt.js';
4
+ import { generateSitemapXml } from './generate-sitemap-xml.js';
5
+
6
+ async function generateSeoFiles() {
7
+ await generateSeoHtml();
8
+ generateRobotsTxt();
9
+ generateSitemapXml();
10
+ }
11
+
12
+ try {
13
+ await generateSeoFiles();
14
+ } catch (err) {
15
+ errorExit(err, 'generate-seo-files');
16
+ }
@@ -0,0 +1,128 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PATHS } from '../app/paths.js';
4
+ import { print, errorExit } from '../../../utils/index.js';
5
+ import { buildSsrBundle, cleanupSsrBundle } from './ssr/ssr-bundle.js';
6
+ import { renderRoute } from './ssr/ssr-render.js';
7
+ import { builderConfig } from '../../builder.config.js';
8
+ import { resolveLocaleValue, valueForLangWithFallback, collectI18nSeoIssues } from './i18n-resolution.js';
9
+ import { segmentsOf, validateMockParams, writeRouteHtml, warnForRoutesMissingSeoConfig } from './route-output.js';
10
+
11
+ async function processRoute(template, baseUrl, defaultLanguage, languages, route, bundleUrl) {
12
+ validateMockParams(route);
13
+
14
+ const routeSuffix = route.path === '/' ? '' : route.path;
15
+
16
+ if (builderConfig.i18n) {
17
+ for (const lang of languages) {
18
+ const title = valueForLangWithFallback(route.title, lang, defaultLanguage);
19
+ const description = valueForLangWithFallback(route.description, lang, defaultLanguage);
20
+ const ogImage = valueForLangWithFallback(route.ogImage, lang, defaultLanguage);
21
+
22
+ const pageUrl = `${baseUrl}/${lang}${routeSuffix}`;
23
+ const body = await renderRoute({
24
+ bundleUrl,
25
+ appRoutesKey: route.path,
26
+ mockParams: route.mockParams,
27
+ mockFetch: route.mockFetch,
28
+ lang,
29
+ pageUrl,
30
+ i18nEnabled: builderConfig.i18n,
31
+ });
32
+
33
+ await writeRouteHtml({
34
+ template, lang, title, description, pageUrl, ogImage, body,
35
+ outputSegments: [lang, ...segmentsOf(routeSuffix)],
36
+ routePath: route.path,
37
+ });
38
+ }
39
+ } else {
40
+ const title = resolveLocaleValue(route.title, defaultLanguage);
41
+ const description = resolveLocaleValue(route.description, defaultLanguage);
42
+ const ogImage = resolveLocaleValue(route.ogImage, defaultLanguage);
43
+
44
+ if (!title || !description) {
45
+ errorExit(`Missing title or description for route "${route.path}" in seo.json`, 'generate-seo-html');
46
+ }
47
+
48
+ const pageUrl = `${baseUrl}${routeSuffix}`;
49
+ const body = await renderRoute({
50
+ bundleUrl,
51
+ appRoutesKey: route.path,
52
+ mockParams: route.mockParams,
53
+ mockFetch: route.mockFetch,
54
+ lang: defaultLanguage,
55
+ pageUrl,
56
+ i18nEnabled: builderConfig.i18n,
57
+ });
58
+
59
+ await writeRouteHtml({
60
+ template, lang: defaultLanguage, title, description, pageUrl, ogImage, body,
61
+ outputSegments: segmentsOf(routeSuffix),
62
+ routePath: route.path,
63
+ });
64
+ }
65
+ }
66
+
67
+ export async function generateSeoHtml() {
68
+ const templatePath = path.join(PATHS.tempSource, 'index.html');
69
+ const template = fs.readFileSync(templatePath, 'utf-8');
70
+
71
+ if (!template.includes('</head>')) {
72
+ errorExit('index.html is missing a </head> tag — cannot inject SEO tags', 'generate-seo-html');
73
+ }
74
+
75
+ const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
76
+ const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
77
+ const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
78
+
79
+ const languagesConfigPath = path.join(PATHS.temp, 'config', 'languages.json');
80
+ const { languages, defaultLanguage } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
81
+
82
+ if (builderConfig.i18n) {
83
+ const { fallbacks, missing, noLocalization } = collectI18nSeoIssues(seoConfig.routes, languages, defaultLanguage);
84
+
85
+ if (noLocalization.length > 0) {
86
+ const lines = noLocalization
87
+ .map(({ path, field }) => ` - route "${path}": "${field}"`)
88
+ .join('\n');
89
+ print.warn(
90
+ `[generate-seo-html] i18n is enabled, but these routes have no language values in seo.json:\n${lines}\n\n` +
91
+ `Add per-language objects (e.g. { "en": ..., "fr": ... }) under the matching route's ` +
92
+ `"title"/"description" in config/seo.json to localize this content, or ignore this ` +
93
+ `warning if sharing the same content across every language is intentional.`
94
+ );
95
+ }
96
+
97
+ if (fallbacks.length > 0) {
98
+ const lines = fallbacks
99
+ .map(({ path, field, lang, sourceLang }) => ` - route "${path}": "${field}.${lang}" not set — using "${field}.${sourceLang}"`)
100
+ .join('\n');
101
+ print.error(`[generate-seo-html] Using defaultLanguage fallback for missing i18n content:\n${lines}`);
102
+ }
103
+
104
+ if (missing.length > 0) {
105
+ const lines = missing
106
+ .map(({ path, field, lang }) => ` - route "${path}": missing "${field}.${lang}" (no language has a value for this field)`)
107
+ .join('\n');
108
+
109
+ errorExit(
110
+ `config/seo.json is missing required content:\n${lines}\n\n` +
111
+ `Add these keys under the matching route's "title"/"description" in config/seo.json. ` +
112
+ `Configured languages (config/languages.json): ${languages.join(', ')}.`
113
+ , 'generate-seo-html'
114
+ );
115
+ }
116
+ }
117
+
118
+ try {
119
+ const bundleUrl = await buildSsrBundle();
120
+ await warnForRoutesMissingSeoConfig(bundleUrl, seoConfig.routes, defaultLanguage, baseUrl);
121
+
122
+ for (const route of seoConfig.routes) {
123
+ await processRoute(template, baseUrl, defaultLanguage, languages, route, bundleUrl);
124
+ }
125
+ } finally {
126
+ cleanupSsrBundle();
127
+ }
128
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PATHS } from '../app/paths.js';
4
+ import { print } from '../../../utils/index.js';
5
+ import { builderConfig } from '../../builder.config.js';
6
+
7
+ function collectUrls(baseUrl, routes, languages) {
8
+ const urls = [];
9
+
10
+ for (const route of routes) {
11
+ const routeSuffix = route.path === '/' ? '' : route.path;
12
+
13
+ if (builderConfig.i18n) {
14
+ for (const lang of languages) {
15
+ urls.push(`${baseUrl}/${lang}${routeSuffix}/`);
16
+ }
17
+ } else {
18
+ urls.push(`${baseUrl}${routeSuffix}/`);
19
+ }
20
+ }
21
+
22
+ return urls;
23
+ }
24
+
25
+ function buildSitemapXml(urls) {
26
+ const urlEntries = urls.map(loc => ` <url>\n <loc>${loc}</loc>\n </url>`).join('\n');
27
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urlEntries}\n</urlset>\n`;
28
+ }
29
+
30
+ export function generateSitemapXml() {
31
+ const seoConfigPath = path.join(PATHS.temp, 'config', 'seo.json');
32
+ const seoConfig = JSON.parse(fs.readFileSync(seoConfigPath, 'utf-8'));
33
+ const baseUrl = seoConfig.baseUrl.replace(/\/$/, '');
34
+
35
+ const languagesConfigPath = path.join(PATHS.temp, 'config', 'languages.json');
36
+ const { languages } = JSON.parse(fs.readFileSync(languagesConfigPath, 'utf-8'));
37
+
38
+ const urls = collectUrls(baseUrl, seoConfig.routes, languages);
39
+ const xml = buildSitemapXml(urls);
40
+
41
+ const outputPath = path.join(PATHS.tempSource, 'sitemap.xml');
42
+ fs.writeFileSync(outputPath, xml, 'utf-8');
43
+ }