@nutin/cli 1.3.1 → 2.1.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 (289) hide show
  1. package/CHANGELOG.md +106 -9
  2. package/README.md +17 -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 +13 -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 +142 -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 +168 -0
  36. package/templates/base/GETTING_STARTED.md.hbs +199 -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 +44 -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 +6 -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 +2 -57
  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 +5 -38
  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 +96 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +6 -3
  57. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +151 -0
  58. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +8 -2
  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 +89 -0
  63. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +4 -1
  64. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +142 -0
  65. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +67 -22
  66. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +183 -0
  67. package/templates/base/src/core/base-classes/component/component.test.js.hbs +85 -0
  68. package/templates/base/src/core/base-classes/component/component.ts.hbs +24 -33
  69. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +58 -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 +71 -0
  74. package/templates/base/src/core/base-classes/view/view.ts.hbs +24 -49
  75. package/templates/base/src/core/config.ts.hbs +10 -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 +187 -0
  87. package/templates/base/src/core/services/globals/globals.ts.hbs +104 -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 +73 -2
  91. package/templates/base/src/core/services/http-client/http-client.ts.hbs +4 -1
  92. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +270 -0
  93. package/templates/{features/i18n → base}/src/core/services/i18n/i18n.ts.hbs +22 -4
  94. package/templates/base/src/core/services/index.ts.hbs +5 -6
  95. package/templates/base/src/core/services/pipe-registry/pipe-registry.test.js.hbs +60 -1
  96. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +1 -1
  97. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +128 -0
  98. package/templates/base/src/{libs/pipes → core/services/pipe-registry}/pipes.ts.hbs +8 -5
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +257 -6
  100. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +56 -29
  101. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +27 -0
  102. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +83 -17
  103. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +14 -38
  104. package/templates/base/src/core/services/router/router.test.js.hbs +304 -2
  105. package/templates/base/src/core/services/router/router.ts.hbs +47 -12
  106. package/templates/base/src/index.html.hbs +2 -6
  107. package/templates/base/src/styles/_styles.scss.hbs +102 -43
  108. package/templates/base/src/styles/base/_variables.scss.hbs +1 -86
  109. package/templates/base/src/styles/main.scss.hbs +1 -18
  110. package/templates/base/src/styles/tailwind.css.hbs +2 -0
  111. package/templates/base/tools/builder/builder.config.js.hbs +40 -0
  112. package/templates/base/tools/builder/builder.js.hbs +24 -26
  113. package/templates/base/tools/builder/core/app/binary-extensions.test.js.hbs +16 -0
  114. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +16 -0
  115. package/templates/base/tools/builder/core/{copy-static.js.hbs → app/copy-static.js.hbs} +27 -34
  116. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +77 -0
  117. package/templates/base/tools/builder/core/finalize-build.js.hbs +16 -7
  118. package/templates/base/tools/builder/core/{add-tags.js.hbs → html-index/add-tags.js.hbs} +2 -2
  119. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +56 -0
  120. package/templates/base/tools/builder/core/{validate-html.js.hbs → html-index/validate-html.js.hbs} +5 -11
  121. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +152 -0
  122. package/templates/base/tools/builder/core/html-templates/minify-html.js.hbs +15 -0
  123. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +8 -0
  124. package/templates/{features/i18n/tools/builder/core → base/tools/builder/core/i18n}/build-i18n.js.hbs +6 -17
  125. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +37 -0
  126. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +30 -0
  127. package/templates/base/tools/builder/core/{hash-files.js.hbs → prod-bundle/hash-files.js.hbs} +14 -11
  128. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +51 -0
  129. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +16 -0
  130. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +128 -0
  131. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +43 -0
  132. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +99 -0
  133. package/templates/base/tools/builder/core/seo/i18n-resolution.js.hbs +66 -0
  134. package/templates/base/tools/builder/core/seo/route-output.js.hbs +64 -0
  135. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +54 -0
  136. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +132 -0
  137. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +95 -0
  138. package/templates/base/tools/builder/core/styles/sass.js.hbs +40 -0
  139. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +84 -0
  140. package/templates/base/tools/dev/dev-serve.js.hbs +26 -18
  141. package/templates/base/tools/dev/serve.js.hbs +11 -1
  142. package/templates/base/tools/dev/watcher.js.hbs +14 -6
  143. package/templates/base/tools/generator/generator.js.hbs +44 -47
  144. package/templates/base/tools/generator/handle-file.js.hbs +17 -44
  145. package/templates/base/tools/generator/handle-file.test.js.hbs +72 -0
  146. package/templates/base/tools/generator/templates/{component.template.js.hbs → component/component.template.js.hbs} +3 -2
  147. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +23 -0
  148. package/templates/base/tools/generator/templates/html/html.template.js.hbs +2 -0
  149. package/templates/base/tools/generator/templates/html/html.template.test.js.hbs +7 -0
  150. package/templates/base/tools/generator/templates/index.js.hbs +7 -12
  151. package/templates/base/tools/generator/templates/locale/locale.template.js.hbs +8 -0
  152. package/templates/base/tools/generator/templates/locale/locale.template.test.js.hbs +17 -0
  153. package/templates/base/tools/generator/templates/scss/scss.template.js.hbs +1 -0
  154. package/templates/base/tools/generator/templates/scss/scss.template.test.js.hbs +7 -0
  155. package/templates/base/tools/generator/templates/service/service.template.js.hbs +17 -0
  156. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +22 -0
  157. package/templates/base/tools/generator/templates/test/test.template.js.hbs +10 -0
  158. package/templates/base/tools/generator/templates/test/test.template.test.js.hbs +27 -0
  159. package/templates/base/tools/generator/templates/{view.template.js.hbs → view/view.template.js.hbs} +4 -3
  160. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +24 -0
  161. package/templates/base/tools/testin-nutin/core/coverage/collect-coverage.js.hbs +39 -0
  162. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.js.hbs +115 -0
  163. package/templates/base/tools/testin-nutin/core/coverage/compute-coverage.test.js.hbs +74 -0
  164. package/templates/base/tools/testin-nutin/core/coverage/write-summary-report.js.hbs +54 -0
  165. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +44 -0
  166. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/assertion-lib.js.hbs +62 -24
  167. package/templates/base/tools/testin-nutin/core/globals/clock.js.hbs +206 -0
  168. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/jsdom-setup.js.hbs +19 -6
  169. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/register-test-globals.js.hbs +13 -4
  170. package/templates/base/tools/testin-nutin/core/globals/silence-console.js.hbs +23 -0
  171. package/templates/{features/testinNutin/testin-nutin/core/src → base/tools/testin-nutin/core}/globals/spyon.js.hbs +2 -0
  172. package/templates/base/tools/testin-nutin/core/index.js.hbs +9 -0
  173. package/templates/base/tools/testin-nutin/core/printer.js.hbs +85 -0
  174. package/templates/base/tools/testin-nutin/core/queue/queue.test.js.hbs +46 -0
  175. package/templates/base/tools/testin-nutin/core/queue/test-discovery.test.js.hbs +46 -0
  176. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +154 -0
  177. package/templates/{features/testinNutin → base/tools}/testin-nutin/core/tests/assertions.test.js.hbs +27 -0
  178. package/templates/base/tools/testin-nutin/core/tests/clock.test.js.hbs +118 -0
  179. package/templates/base/tools/testin-nutin/core/tests/globals-each-all.test.js.hbs +28 -0
  180. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/create-mock-method.js.hbs +2 -1
  181. package/templates/base/tools/testin-nutin/mocks/create-mock-method.test.js.hbs +64 -0
  182. package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-event-bus.js.hbs +3 -2
  183. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +66 -0
  184. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +26 -0
  185. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +61 -0
  186. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +29 -0
  187. package/templates/base/tools/testin-nutin/runner.js.hbs +100 -0
  188. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +37 -0
  189. package/templates/base/tools/utils/error-exit.js.hbs +8 -0
  190. package/templates/base/tools/utils/format.js.hbs +10 -10
  191. package/templates/base/tools/utils/format.test.js.hbs +45 -0
  192. package/templates/base/tools/utils/get-files-recursive.js.hbs +6 -1
  193. package/templates/base/tools/utils/get-files-recursive.test.js.hbs +49 -0
  194. package/templates/base/tools/utils/get-rel-to-core.test.js.hbs +18 -0
  195. package/templates/base/tools/utils/index.js.hbs +1 -3
  196. package/templates/base/tools/utils/languages.js.hbs +15 -0
  197. package/templates/base/tools/utils/languages.test.js.hbs +16 -0
  198. package/templates/base/tools/utils/print.js.hbs +19 -13
  199. package/templates/base/tools/utils/print.test.js.hbs +60 -0
  200. package/templates/base/tools/utils/prompt-boolean.js.hbs +7 -0
  201. package/templates/base/tools/utils/prompt-boolean.test.js.hbs +54 -0
  202. package/templates/base/tools/utils/run-command.js.hbs +4 -2
  203. package/templates/base/tools/utils/run-command.test.js.hbs +19 -0
  204. package/templates/base/tools/utils/run-script.js.hbs +14 -3
  205. package/templates/base/tools/utils/run-script.test.js.hbs +40 -0
  206. package/templates/features/{deployHelper → docker}/.dockerignore.hbs +4 -4
  207. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +51 -0
  208. package/templates/features/{deployHelper/tools/deployment/nginx.conf.hbs → docker/tools/docker/nginx.conf.template} +4 -2
  209. package/templates/features/docker/tools/docker/scripts/docker-build.js.hbs +20 -0
  210. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +48 -0
  211. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +52 -0
  212. package/templates/features/docker/tools/docker/scripts/docker-run.js.hbs +28 -0
  213. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +23 -0
  214. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +60 -0
  215. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +30 -0
  216. package/lib/src/context-builder.mjs +0 -40
  217. package/lib/src/file-generator.mjs +0 -109
  218. package/lib/src/json-generator.mjs +0 -31
  219. package/lib/src/package-manager.mjs +0 -139
  220. package/lib/src/print.mjs +0 -32
  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 -36
  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 -113
  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 -131
  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/helpers/focus-trap.helper.ts.hbs +0 -197
  239. package/templates/base/src/libs/popover/helpers/popover-dom.helper.ts.hbs +0 -75
  240. package/templates/base/src/libs/popover/popover.ts.hbs +0 -138
  241. package/templates/base/src/libs/snackbar/snackbar.ts.hbs +0 -75
  242. package/templates/base/src/styles/base/_functions.scss.hbs +0 -9
  243. package/templates/base/src/styles/core/_anchors.scss.hbs +0 -10
  244. package/templates/base/src/styles/core/_index.scss.hbs +0 -6
  245. package/templates/base/src/styles/core/_popover.scss.hbs +0 -96
  246. package/templates/base/src/styles/core/_snackbar.scss.hbs +0 -50
  247. package/templates/base/tools/builder/core/esbuild.js.hbs +0 -25
  248. package/templates/base/tools/builder/core/minify-html.js.hbs +0 -60
  249. package/templates/base/tools/builder/core/sass.js.hbs +0 -22
  250. package/templates/base/tools/builder/core/tsc.js.hbs +0 -12
  251. package/templates/base/tools/generator/templates/service.template.js.hbs +0 -17
  252. package/templates/base/tools/utils/export-flags.js.hbs +0 -6
  253. package/templates/features/deployHelper/tools/builder/core/compress-files.js.hbs +0 -56
  254. package/templates/features/deployHelper/tools/deployment/Dockerfile.hbs +0 -30
  255. package/templates/features/i18n/src/app/views/home/locales/en.json.hbs +0 -9
  256. package/templates/features/i18n/src/app/views/not-found/locales/en.json +0 -8
  257. package/templates/features/i18n/src/app/views/not-found/locales/fr.json +0 -8
  258. package/templates/features/i18n/src/core/config.ts.hbs +0 -3
  259. package/templates/features/i18n/src/core/services/i18n/i18n.test.js.hbs +0 -125
  260. package/templates/features/i18n/tools/generator/templates/json.template.js.hbs +0 -8
  261. package/templates/features/i18n/tools/utils/languages.js.hbs +0 -7
  262. package/templates/features/stylinNutin/src/styles/_custom-classes.scss.hbs +0 -9
  263. package/templates/features/stylinNutin/src/styles/base/_mixins.scss.hbs +0 -372
  264. package/templates/features/stylinNutin/src/styles/components/_index.scss.hbs +0 -3
  265. package/templates/features/stylinNutin/src/styles/core/_utilities.scss.hbs +0 -254
  266. package/templates/features/stylinNutin/src/styles/views/_index.scss.hbs +0 -0
  267. package/templates/features/stylinNutin/tools/generator/templates/scss.template.js.hbs +0 -4
  268. package/templates/features/template/src/app/views/home/home.view.html.hbs +0 -56
  269. package/templates/features/template/src/app/views/not-found/not-found.view.html.hbs +0 -19
  270. package/templates/features/template/tools/builder/core/merge-templates.js.hbs +0 -49
  271. package/templates/features/template/tools/generator/templates/html.template.js.hbs +0 -2
  272. package/templates/features/testinNutin/testin-nutin/core/index.js.hbs +0 -5
  273. package/templates/features/testinNutin/testin-nutin/core/src/utils/print.js.hbs +0 -64
  274. package/templates/features/testinNutin/testin-nutin/core/src/utils/test-queue.js.hbs +0 -64
  275. package/templates/features/testinNutin/testin-nutin/core/tests/globals-each-all.test.js.hbs +0 -28
  276. package/templates/features/testinNutin/testin-nutin/mocks/mock-store.js.hbs +0 -38
  277. package/templates/features/testinNutin/testin-nutin/runner.js.hbs +0 -36
  278. package/templates/features/testinNutin/testin-nutin/watch-tests.js.hbs +0 -21
  279. package/templates/features/testinNutin/testin-nutin.config.js.hbs +0 -14
  280. /package/lib/src/{template-compiler.mjs → common/template-compiler.mjs} +0 -0
  281. /package/templates/{features/i18n → base}/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +0 -0
  282. /package/templates/{features/i18n → base}/src/core/services/i18n/languages.ts.hbs +0 -0
  283. /package/templates/base/tools/builder/{variables → core/app}/binary-extensions.js.hbs +0 -0
  284. /package/templates/base/tools/builder/core/{paths.js.hbs → app/paths.js.hbs} +0 -0
  285. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/queue.js.hbs +0 -0
  286. /package/templates/{features/testinNutin/testin-nutin/core/src/utils → base/tools/testin-nutin/core/queue}/test-discovery.js.hbs +0 -0
  287. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-http-client.js.hbs +0 -0
  288. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-i18n.js.hbs +0 -0
  289. /package/templates/{features/testinNutin → base/tools}/testin-nutin/mocks/mock-router.js.hbs +0 -0
@@ -0,0 +1,26 @@
1
+ import { MockHttpClient } from './mock-http-client.js';
2
+
3
+ describe('MockHttpClient', () => {
4
+ it('exposes get/post/put/patch/delete as independent bare mocks', () => {
5
+ const client = new MockHttpClient();
6
+ client.get.mockReturnValue('get-result');
7
+ client.post('/widgets', { name: 'a' });
8
+
9
+ expect(client.get()).toBe('get-result');
10
+ expect(client.post.calls).toEqual([['/widgets', { name: 'a' }]]);
11
+ expect(client.put.calls).toEqual([]);
12
+ });
13
+
14
+ it('reset() clears calls and configured return values on every method', () => {
15
+ const client = new MockHttpClient();
16
+ client.get.mockReturnValue('cached');
17
+ client.get();
18
+ client.delete('/widgets/1');
19
+
20
+ client.reset();
21
+
22
+ expect(client.get.calls).toEqual([]);
23
+ expect(client.delete.calls).toEqual([]);
24
+ expect(client.get()).toBe(undefined);
25
+ });
26
+ });
@@ -0,0 +1,61 @@
1
+ import { MockI18n } from './mock-i18n.js';
2
+
3
+ describe('MockI18n', () => {
4
+ it('exposes the constructor defaults via currentLanguage', () => {
5
+ const i18n = new MockI18n('fr', ['en', 'fr']);
6
+ expect(i18n.currentLanguage).toBe('fr');
7
+ });
8
+
9
+ it('translate/getBrowserLanguage run their built-in default implementation without needing .mockImplementation', () => {
10
+ const i18n = new MockI18n();
11
+
12
+ // No translations set — translate() falls back to returning the key itself.
13
+ expect(i18n.translate('some.key')).toBe('some.key');
14
+ expect(i18n.getBrowserLanguage()).toBe('en');
15
+ });
16
+
17
+ it('loadTranslations/initTranslations run their default implementation and update currentLanguage', async () => {
18
+ const i18n = new MockI18n('en', ['en', 'fr']);
19
+
20
+ await i18n.loadTranslations('fr');
21
+ expect(i18n.currentLanguage).toBe('fr');
22
+ });
23
+
24
+ it('translate is still trackable as a mock alongside its default implementation', () => {
25
+ const i18n = new MockI18n();
26
+ i18n.translate('some.key');
27
+
28
+ expect(i18n.translate).toHaveBeenCalledWith('some.key');
29
+ });
30
+
31
+ it('translate can be given real interpolation behavior via .mockImplementation', () => {
32
+ const i18n = new MockI18n();
33
+ i18n.setTranslations({ greeting: 'Hello {name}' });
34
+ i18n.translate.mockImplementation((key, params) => {
35
+ const value = i18n._translations[key] || key;
36
+ if (!params) return value;
37
+ return Object.entries(params).reduce((acc, [k, v]) => acc.replace(`{${k}}`, v), value);
38
+ });
39
+
40
+ expect(i18n.translate('greeting', { name: 'Ada' })).toBe('Hello Ada');
41
+ });
42
+
43
+ it('setTranslations/setDefaultTranslations seed real, readable state', () => {
44
+ const i18n = new MockI18n();
45
+ i18n.setTranslations({ a: '1' });
46
+ i18n.setDefaultTranslations({ b: '2' });
47
+
48
+ expect(i18n._translations).toEqual({ a: '1' });
49
+ expect(i18n._defaultTranslations).toEqual({ b: '2' });
50
+ });
51
+
52
+ it('reset() restores the constructor default language and clears translations', () => {
53
+ const i18n = new MockI18n('en');
54
+ i18n.setTranslations({ a: '1' });
55
+
56
+ i18n.reset();
57
+
58
+ expect(i18n._translations).toEqual({});
59
+ expect(i18n.currentLanguage).toBe('en');
60
+ });
61
+ });
@@ -0,0 +1,29 @@
1
+ import { MockRouter } from './mock-router.js';
2
+
3
+ describe('MockRouter', () => {
4
+ it('stores the routes passed to the constructor', () => {
5
+ const routes = { '/home': {} };
6
+ const router = new MockRouter(routes);
7
+
8
+ expect(router.routes).toBe(routes);
9
+ });
10
+
11
+ it('navigate/handlePopState/handleNotFound/handleGuards/initializeEventListeners are independent bare mocks', () => {
12
+ const router = new MockRouter();
13
+ router.navigate('/about');
14
+
15
+ expect(router.navigate.calls).toEqual([['/about']]);
16
+ expect(router.handlePopState.calls).toEqual([]);
17
+ });
18
+
19
+ it('reset() clears every mock method and _currentView', () => {
20
+ const router = new MockRouter();
21
+ router.navigate('/about');
22
+ router._currentView = 'about-view';
23
+
24
+ router.reset();
25
+
26
+ expect(router.navigate.calls).toEqual([]);
27
+ expect(router._currentView).toBe(null);
28
+ });
29
+ });
@@ -0,0 +1,100 @@
1
+ import path from 'path';
2
+ import {
3
+ registerTestGlobals, getTestFiles, runQueuedTests, setCurrentTestFile, print,
4
+ startCoverage, stopCoverage, buildCoverageReport, printCoverageReport, maybeWriteUncoveredReport,
5
+ writeSummaryReport, setupJsdom, teardownJsdom,
6
+ } from './core/index.js';
7
+ import config from '#root/nutin.config.js';
8
+
9
+ const args = process.argv.slice(2);
10
+ const coverageEnabled = args.includes('--coverage') || Boolean(config.testinNutin.coverage?.enabled);
11
+ const verboseEnabled = args.includes('--verbose');
12
+ const fileFilters = args.filter(arg => arg !== '--coverage' && arg !== '--verbose');
13
+
14
+ // Tests import and run the compiled build output (dist/src/)
15
+ // and has no sourcemap back to the .ts sources
16
+ const origins = [];
17
+ const coverageDirs = [];
18
+
19
+ if (config.testinNutin.includeFramework) {
20
+ const frameworkDir = path.join('src', 'core');
21
+ origins.push(frameworkDir);
22
+ if (coverageEnabled) coverageDirs.push(path.join('dist', frameworkDir));
23
+ }
24
+
25
+ if (config.testinNutin.includeApp) {
26
+ const appDir = path.join('src', 'app');
27
+ origins.push(appDir);
28
+ if (coverageEnabled) coverageDirs.push(path.join('dist', appDir));
29
+ }
30
+
31
+ if (config.testinNutin.includeTools) origins.push('tools');
32
+
33
+ registerTestGlobals();
34
+
35
+ let testFiles = origins.flatMap(
36
+ origin => getTestFiles(path.join('..', origin))
37
+ );
38
+
39
+ if (fileFilters.length) {
40
+ testFiles = testFiles.filter(file => fileFilters.some(f => file.includes(f)));
41
+ }
42
+
43
+ async function loadTestFile(file) {
44
+ setupJsdom();
45
+ setCurrentTestFile(file);
46
+ await import(file);
47
+ teardownJsdom();
48
+ }
49
+
50
+ async function runCoverage(testResults) {
51
+ let scopedResults = await stopCoverage(coverageDirs);
52
+ if (fileFilters.length) {
53
+ scopedResults = scopedResults.filter(({ filePath }) => fileFilters.some(f => filePath.includes(f)));
54
+ }
55
+ const report = buildCoverageReport(scopedResults);
56
+ const globals = printCoverageReport(report);
57
+ writeSummaryReport(report, globals, testResults);
58
+ await maybeWriteUncoveredReport(report);
59
+
60
+ const threshold = config.testinNutin.coverage?.threshold;
61
+ if (typeof threshold === 'number') {
62
+ const misses = Object.entries({
63
+ lines: globals.globalLinesPct,
64
+ functions: globals.globalFunctionsPct,
65
+ branches: globals.globalBranchesPct,
66
+ }).filter(([, pct]) => pct < threshold);
67
+
68
+ if (misses.length) {
69
+ print.boldError(`\n✗ Coverage below threshold (${threshold}%): ${misses.map(([k, pct]) => `${k} ${pct.toFixed(1)}%`).join(', ')}\n`);
70
+ process.exit(1);
71
+ }
72
+ }
73
+ }
74
+
75
+ async function runTests() {
76
+ if (verboseEnabled) print.head('Test suites :');
77
+
78
+ if (coverageEnabled) await startCoverage();
79
+
80
+ for (const file of testFiles) {
81
+ try {
82
+ await loadTestFile(file);
83
+ } catch (error) {
84
+ print.boldError(`✗ ${file} failed to load:`);
85
+ print.grayError(error.stack);
86
+ }
87
+ }
88
+
89
+ const testResults = await runQueuedTests();
90
+
91
+ if (testResults.failed > 0) process.exitCode = 1;
92
+
93
+ if (coverageEnabled) await runCoverage(testResults);
94
+ }
95
+
96
+ runTests().catch((err) => {
97
+ print.boldError(`Unexpected error: ${err.message}`);
98
+ print.grayError(err.stack);
99
+ process.exitCode = 1;
100
+ });
@@ -0,0 +1,37 @@
1
+ import chokidar from 'chokidar';
2
+ import { exec } from 'child_process';
3
+ import path from 'path';
4
+ import { print } from './core/index.js'
5
+
6
+ const watcher = chokidar.watch(['src', 'test', 'unit', 'e2e'], {
7
+ ignored: /(^|[/\\])\../,
8
+ persistent: true,
9
+ });
10
+
11
+ print.boldHead('Watching for test changes...')
12
+
13
+ watcher.on('error', (err) => {
14
+ print.boldError(`\nWatcher error: ${err.message}`);
15
+ });
16
+
17
+ let isRunning = false;
18
+ let runTimeout = null;
19
+
20
+ watcher.on('change', (filePath) => {
21
+ if (runTimeout) clearTimeout(runTimeout);
22
+
23
+ runTimeout = setTimeout(() => {
24
+ if (isRunning) return;
25
+ isRunning = true;
26
+
27
+ console.clear();
28
+ print.boldInfo(`\n🔄 File changed: ${path.relative(process.cwd(), filePath)}\n`);
29
+
30
+ exec('node --experimental-modules testin-nutin/runner.js', { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
31
+ if (stdout) process.stdout.write(stdout);
32
+ if (stderr) process.stderr.write(stderr);
33
+ if (err) print.boldError(`\nTest run failed: ${err.message}`);
34
+ isRunning = false;
35
+ });
36
+ }, 100);
37
+ });
@@ -0,0 +1,8 @@
1
+ import { exit } from 'process';
2
+ import { print } from './print.js';
3
+
4
+ export function errorExit(err, origin = '') {
5
+ print.boldError(`\n${origin} - Error: ${ err.message || err }`);
6
+ if (err.stack) print.grayError(err.stack)
7
+ exit(1);
8
+ }
@@ -5,20 +5,20 @@ export function getLastWord(input) {
5
5
  }
6
6
 
7
7
  export function capitalized(input) {
8
- const formatted = input.replace(
8
+ const formatted = input.replace(
9
9
  /\w*/g,
10
10
  function(txt) {
11
11
  return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
12
12
  })
13
- return formatted.replace(/,/g, '');
13
+ return formatted.replace(/,/g, '');
14
14
  }
15
15
 
16
16
  export function kebabCased(input) {
17
- return input.replace(/[A-Z][a-z]*/g, str => str.toLowerCase() + ' ').trim().replace(/ /g, '-').replace(/,/g, '');
17
+ return input.replace(/[A-Z][a-z]*/g, str => str.toLowerCase() + ' ').trim().replace(/ /g, '-').replace(/,/g, '');
18
18
  }
19
19
 
20
20
  export function camelCased(input) {
21
- return input
21
+ return input
22
22
  .split('-')
23
23
  .map((word, index) =>
24
24
  index === 0
@@ -36,10 +36,10 @@ export function pascalCased(input) {
36
36
  }
37
37
 
38
38
  export function allFormats(input) {
39
- return {
40
- kebab: kebabCased(input),
41
- pascal: pascalCased(input),
42
- camel: camelCased(input),
43
- capitalized: capitalized(input)
44
- }
39
+ return {
40
+ kebab: kebabCased(input),
41
+ pascal: pascalCased(input),
42
+ camel: camelCased(input),
43
+ capitalized: capitalized(input)
44
+ }
45
45
  }
@@ -0,0 +1,45 @@
1
+ import { getLastWord, capitalized, kebabCased, camelCased, pascalCased, allFormats } from './format.js';
2
+
3
+ describe('format', () => {
4
+ it('getLastWord returns the last non-empty path segment', () => {
5
+ expect(getLastWord('a/b/c')).toBe('c');
6
+ expect(getLastWord('a/b/c/')).toBe('c');
7
+ expect(getLastWord('single')).toBe('single');
8
+ });
9
+
10
+ it('getLastWord returns an empty string for non-string input', () => {
11
+ expect(getLastWord(123)).toBe('');
12
+ expect(getLastWord(undefined)).toBe('');
13
+ });
14
+
15
+ it('capitalized title-cases each word and strips commas', () => {
16
+ expect(capitalized('hello world')).toBe('Hello World');
17
+ expect(capitalized('HELLO, WORLd')).toBe('Hello World');
18
+ });
19
+
20
+ it('kebabCased splits on capital letters', () => {
21
+ expect(kebabCased('HelloWorld')).toBe('hello-world');
22
+ // leading lowercase run isn't matched by the [A-Z][a-z]* regex, so it
23
+ // sticks to the next captured word instead of being split off on its own
24
+ expect(kebabCased('myComponentName')).toBe('mycomponent-name');
25
+ });
26
+
27
+ it('camelCased joins kebab-case segments with an initial lowercase', () => {
28
+ expect(camelCased('my-component-name')).toBe('myComponentName');
29
+ expect(camelCased('single')).toBe('single');
30
+ });
31
+
32
+ it('pascalCased joins kebab-case segments with every segment capitalized', () => {
33
+ expect(pascalCased('my-component-name')).toBe('MyComponentName');
34
+ expect(pascalCased('single')).toBe('Single');
35
+ });
36
+
37
+ it('allFormats returns all four casings for the same input', () => {
38
+ expect(allFormats('my-component')).toEqual({
39
+ kebab: 'my-component',
40
+ pascal: 'MyComponent',
41
+ camel: 'myComponent',
42
+ capitalized: 'My-Component',
43
+ });
44
+ });
45
+ });
@@ -16,7 +16,12 @@ export function getFilesRecursive(dir, extension) {
16
16
 
17
17
  function findFilesRecursively(currentDir) {
18
18
  const files = [];
19
- const items = fs.readdirSync(currentDir, { withFileTypes: true });
19
+ let items;
20
+ try {
21
+ items = fs.readdirSync(currentDir, { withFileTypes: true });
22
+ } catch (err) {
23
+ throw new Error(`Failed to read directory "${currentDir}": ${err.message}`, { cause: err });
24
+ }
20
25
 
21
26
  for (const item of items) {
22
27
  const fullPath = path.join(currentDir, item.name);
@@ -0,0 +1,49 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { getFilesRecursive } from './get-files-recursive.js';
5
+
6
+ describe('getFilesRecursive', () => {
7
+ let tmpDir;
8
+
9
+ beforeEach(() => {
10
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nutin-get-files-recursive-'));
11
+ fs.mkdirSync(path.join(tmpDir, 'nested'), { recursive: true });
12
+ fs.writeFileSync(path.join(tmpDir, 'a.js'), '');
13
+ fs.writeFileSync(path.join(tmpDir, 'b.css'), '');
14
+ fs.writeFileSync(path.join(tmpDir, 'nested', 'c.js'), '');
15
+ fs.writeFileSync(path.join(tmpDir, 'nested', 'd.txt'), '');
16
+ });
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(tmpDir, { recursive: true, force: true });
20
+ });
21
+
22
+ it('recursively collects files matching a single extension', () => {
23
+ const files = getFilesRecursive(tmpDir, '.js').sort();
24
+ expect(files).toEqual([
25
+ path.join(tmpDir, 'a.js'),
26
+ path.join(tmpDir, 'nested', 'c.js'),
27
+ ].sort());
28
+ });
29
+
30
+ it('accepts an extension without a leading dot', () => {
31
+ const files = getFilesRecursive(tmpDir, 'js').sort();
32
+ expect(files).toEqual([
33
+ path.join(tmpDir, 'a.js'),
34
+ path.join(tmpDir, 'nested', 'c.js'),
35
+ ].sort());
36
+ });
37
+
38
+ it('accepts an array of extensions', () => {
39
+ const files = getFilesRecursive(tmpDir, ['.css', '.txt']).sort();
40
+ expect(files).toEqual([
41
+ path.join(tmpDir, 'b.css'),
42
+ path.join(tmpDir, 'nested', 'd.txt'),
43
+ ].sort());
44
+ });
45
+
46
+ it('returns an empty array when the directory does not exist', () => {
47
+ expect(getFilesRecursive(path.join(tmpDir, 'does-not-exist'), '.js')).toEqual([]);
48
+ });
49
+ });
@@ -0,0 +1,18 @@
1
+ import path from 'path';
2
+ import { getRelToCore } from './get-rel-to-core.js';
3
+
4
+ describe('getRelToCore', () => {
5
+ it('adds one "../" per path segment before core/index.js', () => {
6
+ const targetPath = ['src', 'app', 'components', 'widget'].join(path.sep);
7
+ expect(getRelToCore(targetPath)).toBe('../../../core/index.js');
8
+ });
9
+
10
+ it('adds no "../" for a single-segment path', () => {
11
+ expect(getRelToCore('widget')).toBe('core/index.js');
12
+ });
13
+
14
+ it('scales with the number of segments', () => {
15
+ const targetPath = ['a', 'b'].join(path.sep);
16
+ expect(getRelToCore(targetPath)).toBe('../core/index.js');
17
+ });
18
+ });
@@ -3,8 +3,6 @@ export * from './format.js';
3
3
  export * from './get-files-recursive.js';
4
4
  export * from './run-command.js';
5
5
  export * from './run-script.js';
6
- export * from './export-flags.js';
7
6
  export * from './prompt-boolean.js';
8
- {{#if i18n}}
9
7
  export * from './languages.js';
10
- {{/if}}
8
+ export * from './error-exit.js';
@@ -0,0 +1,15 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { errorExit } from './error-exit.js';
4
+
5
+ const langsPath = path.join(process.cwd(), 'config', 'languages.json');
6
+
7
+ let langs;
8
+ try {
9
+ langs = JSON.parse(fs.readFileSync(langsPath, 'utf-8'));
10
+ } catch (err) {
11
+ errorExit(new Error(`config/languages.json is missing or malformed: ${err.message}`), 'languages');
12
+ }
13
+
14
+ export const LANGUAGES = langs.languages;
15
+ export const DEFAULT_LANGUAGE = langs.defaultLanguage;
@@ -0,0 +1,16 @@
1
+ import { LANGUAGES, DEFAULT_LANGUAGE } from './languages.js';
2
+
3
+ describe('languages', () => {
4
+ it('reads LANGUAGES from config/languages.json', () => {
5
+ expect(Array.isArray(LANGUAGES)).toBeTruthy();
6
+ expect(LANGUAGES.length).toBeGreaterThan(0);
7
+ });
8
+
9
+ it('reads DEFAULT_LANGUAGE from config/languages.json', () => {
10
+ expect(typeof DEFAULT_LANGUAGE).toBe('string');
11
+ });
12
+
13
+ it('includes the default language among the supported languages', () => {
14
+ expect(LANGUAGES).toContain(DEFAULT_LANGUAGE);
15
+ });
16
+ });
@@ -12,20 +12,26 @@ export const chalk = {
12
12
  boldYellow: (s) => `\x1b[1;33m${s}\x1b[0m`,
13
13
  boldCyan: (s) => `\x1b[1;36m${s}\x1b[0m`,
14
14
  boldPurple: (s) => `\x1b[1;35m${s}\x1b[0m`,
15
+ boldGray: (s) => `\x1b[1;90m${s}\x1b[0m`,
15
16
  };
16
17
 
17
18
  export const print = {
18
- head: (s) => console.log(`${chalk.purple(s)}`),
19
- boldHead : (s) => console.log(`${chalk.boldPurple(s)}`),
20
- section: (s) => console.log(`${chalk.yellow(s)}`),
21
- boldSection: (s) => console.log(`${chalk.boldYellow(s)}`),
22
- info: (s) => console.log(`${chalk.green(s)}`),
23
- boldInfo: (s) => console.log(`${chalk.boldGreen(s)}`),
24
- success: (s) => console.log(`${chalk.cyan(s)}`),
25
- boldSuccess: (s) => console.log(`${chalk.boldCyan(s)}`),
26
- error: (s) => console.error(`${chalk.red(s)}`),
27
- boldError: (s) => console.error(`${chalk.boldRed(s)}`),
28
- grayError: (s) => console.error(`${chalk.gray(s)}`),
29
- blue: (s) => console.log(`${chalk.blue(s)}`),
30
- boldBlue: (s) => console.log(`${chalk.boldBlue(s)}`),
19
+ head: (s) => console.info(`${chalk.purple(s)}`),
20
+ boldHead : (s) => console.info(`${chalk.boldPurple(s)}`),
21
+ section: (s) => console.info(`${chalk.yellow(s)}`),
22
+ boldSection: (s) => console.info(`${chalk.boldYellow(s)}`),
23
+ info: (s) => console.info(`${chalk.green(s)}`),
24
+ boldInfo: (s) => console.info(`${chalk.boldGreen(s)}`),
25
+ success: (s) => console.log(`${chalk.cyan(s)}`),
26
+ boldSuccess: (s) => console.log(`${chalk.boldCyan(s)}`),
27
+ gray: (s) => console.info(`${chalk.gray(s)}`),
28
+ boldGray: (s) => console.info(`${chalk.boldGray(s)}`),
29
+ blue: (s) => console.info(`${chalk.blue(s)}`),
30
+ boldBlue: (s) => console.info(`${chalk.boldBlue(s)}`),
31
+ error: (s) => console.error(`${chalk.red(s)}`),
32
+ boldError: (s) => console.error(`${chalk.boldRed(s)}`),
33
+ grayError: (s) => console.error(`${chalk.boldGray(s)}`),
34
+ warn: (s) => console.warn(`${chalk.yellow(s)}`),
35
+ boldWarn: (s) => console.warn(`${chalk.yellow(s)}`),
36
+ debug: (s) => console.debug(`${chalk.gray(s)}`),
31
37
  }
@@ -0,0 +1,60 @@
1
+ import { print, chalk } from './print.js';
2
+
3
+ describe('print', () => {
4
+ let infoSpy;
5
+ let logSpy;
6
+ let errorSpy;
7
+ let warnSpy;
8
+ let debugSpy;
9
+
10
+ beforeEach(() => {
11
+ infoSpy = spyOn(console, 'info').andCallFake(() => {});
12
+ logSpy = spyOn(console, 'log').andCallFake(() => {});
13
+ errorSpy = spyOn(console, 'error').andCallFake(() => {});
14
+ warnSpy = spyOn(console, 'warn').andCallFake(() => {});
15
+ debugSpy = spyOn(console, 'debug').andCallFake(() => {});
16
+ });
17
+
18
+ afterEach(() => {
19
+ infoSpy.restore();
20
+ logSpy.restore();
21
+ errorSpy.restore();
22
+ warnSpy.restore();
23
+ debugSpy.restore();
24
+ });
25
+
26
+ it('info() writes to console.info, colorized green', () => {
27
+ print.info('hello');
28
+ expect(infoSpy.callCount).toBe(1);
29
+ expect(infoSpy.lastCall).toEqual([chalk.green('hello')]);
30
+ });
31
+
32
+ it('success() writes to console.log, colorized cyan', () => {
33
+ print.success('done');
34
+ expect(logSpy.callCount).toBe(1);
35
+ expect(logSpy.lastCall).toEqual([chalk.cyan('done')]);
36
+ });
37
+
38
+ it('error() writes to console.error, colorized red', () => {
39
+ print.error('oops');
40
+ expect(errorSpy.callCount).toBe(1);
41
+ expect(errorSpy.lastCall).toEqual([chalk.red('oops')]);
42
+ });
43
+
44
+ it('warn() writes to console.warn, colorized yellow', () => {
45
+ print.warn('careful');
46
+ expect(warnSpy.callCount).toBe(1);
47
+ expect(warnSpy.lastCall).toEqual([chalk.yellow('careful')]);
48
+ });
49
+
50
+ it('debug() writes to console.debug, colorized gray', () => {
51
+ print.debug('trace');
52
+ expect(debugSpy.callCount).toBe(1);
53
+ expect(debugSpy.lastCall).toEqual([chalk.gray('trace')]);
54
+ });
55
+
56
+ it('chalk wraps text in the matching ANSI escape codes', () => {
57
+ expect(chalk.red('x')).toBe('\x1b[31mx\x1b[0m');
58
+ expect(chalk.boldGreen('x')).toBe('\x1b[1;32mx\x1b[0m');
59
+ });
60
+ });
@@ -7,9 +7,16 @@ export const promptBoolean = (question) => {
7
7
  });
8
8
 
9
9
  return new Promise((resolve) => {
10
+ let answered = false;
11
+
10
12
  rl.question(`${question} (y/n): `, (answer) => {
13
+ answered = true;
11
14
  rl.close();
12
15
  resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
13
16
  });
17
+
18
+ rl.on('close', () => {
19
+ if (!answered) resolve(false);
20
+ });
14
21
  });
15
22
  };
@@ -0,0 +1,54 @@
1
+ import readline from 'readline';
2
+ import { promptBoolean } from './prompt-boolean.js';
3
+
4
+ describe('promptBoolean', () => {
5
+ let createInterfaceSpy;
6
+ let answer;
7
+
8
+ beforeEach(() => {
9
+ createInterfaceSpy = spyOn(readline, 'createInterface').andCallFake(() => ({
10
+ question: (_question, callback) => callback(answer),
11
+ close: () => {},
12
+ on: () => {},
13
+ }));
14
+ });
15
+
16
+ afterEach(() => {
17
+ createInterfaceSpy.restore();
18
+ });
19
+
20
+ it('resolves true for "y"', async () => {
21
+ answer = 'y';
22
+ expect(await promptBoolean('continue?')).toBe(true);
23
+ });
24
+
25
+ it('resolves true for "yes", case-insensitively', async () => {
26
+ answer = 'YES';
27
+ expect(await promptBoolean('continue?')).toBe(true);
28
+ });
29
+
30
+ it('resolves false for "n"', async () => {
31
+ answer = 'n';
32
+ expect(await promptBoolean('continue?')).toBe(false);
33
+ });
34
+
35
+ it('resolves false for anything else', async () => {
36
+ answer = 'maybe';
37
+ expect(await promptBoolean('continue?')).toBe(false);
38
+ });
39
+
40
+ it('resolves false instead of hanging if stdin closes before an answer', async () => {
41
+ createInterfaceSpy.restore();
42
+ let closeHandler;
43
+ createInterfaceSpy = spyOn(readline, 'createInterface').andCallFake(() => ({
44
+ question: () => {},
45
+ close: () => {},
46
+ on: (event, handler) => { if (event === 'close') closeHandler = handler; },
47
+ }));
48
+
49
+ const resultPromise = promptBoolean('continue?');
50
+ closeHandler();
51
+
52
+ expect(await resultPromise).toBe(false);
53
+ });
54
+ });
@@ -1,8 +1,10 @@
1
1
  import { spawn } from 'child_process';
2
2
 
3
- export function runCommand(command, args = []) {
3
+ export function runCommand(command, args = [], options = {}) {
4
4
  return new Promise((resolve, reject) => {
5
- const child = spawn(command, args, { stdio: 'inherit', shell: true });
5
+ const child = spawn([command, ...args].join(' '), { stdio: 'inherit', shell: true, ...options });
6
+
7
+ child.on('error', (err) => reject(err));
6
8
 
7
9
  child.on('close', (code) => {
8
10
  if (code === 0) resolve();