@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
@@ -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
+ }
@@ -0,0 +1,99 @@
1
+ export function escape(str) {
2
+ return str
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&#39;');
8
+ }
9
+
10
+ export function upsertInHead(html, pattern, replacement, fullTag) {
11
+ if (pattern.test(html)) {
12
+ return html.replace(pattern, replacement);
13
+ }
14
+ return html.replace('</head>', `\n ${fullTag}\n </head>`);
15
+ }
16
+
17
+ export function applySubstitutions(template, lang, title, description, pageUrl, ogImage) {
18
+ let html = template;
19
+
20
+ // lang attribute on <html>
21
+ if (/(<html[^>]*\slang=)["'][^"']*["']/.test(html)) {
22
+ html = html.replace(/(<html[^>]*\slang=)["'][^"']*["']/, `$1"${lang}"`);
23
+ } else {
24
+ html = html.replace(/<html/, `<html lang="${lang}"`);
25
+ }
26
+
27
+ // <title>
28
+ html = upsertInHead(html,
29
+ /(<title>)[^<]*(<\/title>)/,
30
+ `$1${escape(title)}$2`,
31
+ `<title>${escape(title)}</title>`
32
+ );
33
+
34
+ // <meta name="description"> — replace all occurrences, or insert one
35
+ const descPattern = /(<meta\s+name=["']description["']\s+content=)["'][^"']*["']/;
36
+ if (descPattern.test(html)) {
37
+ html = html.replace(/(<meta\s+name=["']description["']\s+content=)["'][^"']*["']/g, `$1"${escape(description)}"`);
38
+ } else {
39
+ html = html.replace('</head>', `\n <meta name="description" content="${escape(description)}" />\n </head>`);
40
+ }
41
+
42
+ // <link rel="canonical">
43
+ html = upsertInHead(html,
44
+ /(<link\s+rel=["']canonical["']\s+href=)["'][^"']*["']/,
45
+ `$1"${pageUrl}/"`,
46
+ `<link rel="canonical" href="${pageUrl}/" />`
47
+ );
48
+
49
+ // og:url
50
+ html = upsertInHead(html,
51
+ /(<meta\s+property=["']og:url["']\s+content=)["'][^"']*["']/,
52
+ `$1"${pageUrl}/"`,
53
+ `<meta property="og:url" content="${pageUrl}/" />`
54
+ );
55
+
56
+ // og:title
57
+ html = upsertInHead(html,
58
+ /(<meta\s+property=["']og:title["']\s+content=)["'][^"']*["']/,
59
+ `$1"${escape(title)}"`,
60
+ `<meta property="og:title" content="${escape(title)}" />`
61
+ );
62
+
63
+ // og:description
64
+ html = upsertInHead(html,
65
+ /(<meta\s+property=["']og:description["']\s+content=)["'][^"']*["']/,
66
+ `$1"${escape(description)}"`,
67
+ `<meta property="og:description" content="${escape(description)}" />`
68
+ );
69
+
70
+ // twitter:title
71
+ html = upsertInHead(html,
72
+ /(<meta\s+name=["']twitter:title["']\s+content=)["'][^"']*["']/,
73
+ `$1"${escape(title)}"`,
74
+ `<meta name="twitter:title" content="${escape(title)}" />`
75
+ );
76
+
77
+ // twitter:description
78
+ html = upsertInHead(html,
79
+ /(<meta\s+name=["']twitter:description["']\s+content=)["'][^"']*["']/,
80
+ `$1"${escape(description)}"`,
81
+ `<meta name="twitter:description" content="${escape(description)}" />`
82
+ );
83
+
84
+ // og:image + twitter:image — only when explicitly provided in route config
85
+ if (ogImage) {
86
+ html = upsertInHead(html,
87
+ /(<meta\s+property=["']og:image["']\s+content=)["'][^"']*["']/,
88
+ `$1"${escape(ogImage)}"`,
89
+ `<meta property="og:image" content="${escape(ogImage)}" />`
90
+ );
91
+ html = upsertInHead(html,
92
+ /(<meta\s+name=["']twitter:image["']\s+content=)["'][^"']*["']/,
93
+ `$1"${escape(ogImage)}"`,
94
+ `<meta name="twitter:image" content="${escape(ogImage)}" />`
95
+ );
96
+ }
97
+
98
+ return html;
99
+ }
@@ -0,0 +1,66 @@
1
+ // Collapses a per-lang-object-or-flat value down to one language, falling back to the
2
+ // first available value if the exact key is missing. Used when i18n is off, where a route
3
+ // may still be authored as a per-lang object (e.g. the default seo.json.hbs) or a flat value.
4
+ export function resolveLocaleValue(value, defaultLanguage) {
5
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
6
+ return value[defaultLanguage] ?? Object.values(value)[0];
7
+ }
8
+ return value;
9
+ }
10
+
11
+ // Used when i18n is on: a per-lang object must have an exact entry for `lang` (no silent
12
+ // fallback to another language), while a flat value is applied uniformly to every language.
13
+ function valueForLang(value, lang) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) ? value[lang] : value;
15
+ }
16
+
17
+ // Used to actually resolve HTML content when i18n is on: falls back to defaultLanguage (then
18
+ // the first available value) when `lang` has no exact entry, mirroring resolveLocaleValue's
19
+ // fallback. Missing translations become a warning (see collectI18nSeoIssues), not a crash.
20
+ export function valueForLangWithFallback(value, lang, defaultLanguage) {
21
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
22
+ return value[lang] ?? value[defaultLanguage] ?? Object.values(value)[0];
23
+ }
24
+ return value;
25
+ }
26
+
27
+ // Which language's value valueForLangWithFallback would actually use for `lang`, given the
28
+ // same value[lang] ?? value[defaultLanguage] ?? Object.values(value)[0] precedence — so
29
+ // warnings can name the real source language instead of assuming defaultLanguage.
30
+ function sourceLangFor(value, lang, defaultLanguage) {
31
+ if (value[lang]) return lang;
32
+ if (value[defaultLanguage]) return defaultLanguage;
33
+ return Object.keys(value).find((key) => value[key]);
34
+ }
35
+
36
+ // Upfront pass over every route x lang x field, run before the (expensive) SSR bundle build.
37
+ // Buckets each combination into a fallback warning (some language has content, this one
38
+ // doesn't) or a hard error (no language has any content for this field on this route).
39
+ export function collectI18nSeoIssues(routes, languages, defaultLanguage) {
40
+ const fallbacks = [];
41
+ const missing = [];
42
+ const noLocalization = [];
43
+
44
+ for (const route of routes) {
45
+ for (const field of ['title', 'description']) {
46
+ const value = route[field];
47
+
48
+ if (value && typeof value !== 'object') {
49
+ noLocalization.push({ path: route.path, field });
50
+ }
51
+
52
+ for (const lang of languages) {
53
+ if (valueForLang(value, lang)) continue;
54
+
55
+ const sourceLang = value && typeof value === 'object' ? sourceLangFor(value, lang, defaultLanguage) : undefined;
56
+ if (sourceLang) {
57
+ fallbacks.push({ path: route.path, field, lang, sourceLang });
58
+ } else {
59
+ missing.push({ path: route.path, field, lang });
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ return { fallbacks, missing, noLocalization };
66
+ }
@@ -0,0 +1,64 @@
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 { applySubstitutions } from './html-substitutions.js';
6
+ import { getAppRoutePaths } from './ssr/ssr-render.js';
7
+ import { builderConfig } from '../../builder.config.js';
8
+
9
+ export function segmentsOf(routeSuffix) {
10
+ return routeSuffix ? routeSuffix.split('/').filter(Boolean) : [];
11
+ }
12
+
13
+ export function validateMockParams(route) {
14
+ const requiredParams = [...route.path.matchAll(/:([^/]+)/g)]
15
+ .map(([, name]) => name)
16
+ .filter((name) => !name.endsWith('?')); // optional segments don't require a mock value
17
+
18
+ if (requiredParams.length === 0) return;
19
+
20
+ const provided = route.mockParams ?? {};
21
+ const missing = requiredParams.filter((name) => !(name in provided));
22
+
23
+ if (missing.length > 0) {
24
+ errorExit(
25
+ `Route "${route.path}" has dynamic segment(s) but is missing "mockParams" ` +
26
+ `for: ${missing.join(', ')}. Add a "mockParams" object to this route in config/seo.json.`
27
+ , 'generate-seo-html'
28
+ );
29
+ }
30
+ }
31
+
32
+ export async function writeRouteHtml({ template, lang, title, description, pageUrl, ogImage, body, outputSegments, routePath }) {
33
+ let html = applySubstitutions(template, lang, title, description, pageUrl, ogImage);
34
+
35
+ if (html === template) {
36
+ errorExit(`Failed to apply any changes for ${routePath} (lang "${lang}") — index.html may be missing a </head> tag`, 'generate-seo-html');
37
+ }
38
+
39
+ html = html.replace(
40
+ /<main\s+id=["']app["'][^>]*>[\s\S]*?<\/main>/,
41
+ `<main id="app">\n${body}\n </main>`
42
+ );
43
+
44
+ const outputDir = path.join(PATHS.tempSource, ...outputSegments);
45
+ const outputPath = path.join(outputDir, 'index.html');
46
+
47
+ fs.mkdirSync(outputDir, { recursive: true });
48
+ fs.writeFileSync(outputPath, html, 'utf-8');
49
+ }
50
+
51
+ export async function warnForRoutesMissingSeoConfig(bundleUrl, seoRoutes, defaultLanguage, baseUrl) {
52
+ const appRoutePaths = await getAppRoutePaths(bundleUrl, defaultLanguage, baseUrl);
53
+ const seoRoutePaths = new Set(seoRoutes.map((route) => route.path));
54
+
55
+ for (const path of appRoutePaths) {
56
+ if (seoRoutePaths.has(path)) continue;
57
+
58
+ if (builderConfig.WELL_KNOWN_NON_SEO_ROUTES.includes(path)) {
59
+ continue;
60
+ } else {
61
+ print.error(`[generate-seo-html] Route "${path}" has no matching entry in config/seo.json; consider adding it so this route gets SEO HTML, sitemap, and meta tags.`);
62
+ }
63
+ }
64
+ }