@nutin/cli 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/README.md +14 -6
  3. package/bin/add-feature.mjs +1 -1
  4. package/bin/cli.mjs +1 -1
  5. package/bin/create-app.mjs +1 -1
  6. package/bin/update-app.mjs +1 -1
  7. package/lib/index.mjs +6 -2
  8. package/lib/src/add-feature/base-template-patcher.mjs +139 -0
  9. package/lib/src/add-feature/feature-adder.mjs +20 -9
  10. package/lib/src/add-feature/feature-context-builder.mjs +1 -0
  11. package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
  12. package/lib/src/add-feature/package-json-updater.mjs +7 -9
  13. package/lib/src/common/feature-registry.mjs +4 -0
  14. package/lib/src/common/file-generator.mjs +25 -3
  15. package/lib/src/common/git.mjs +31 -0
  16. package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
  17. package/lib/src/common/package-json-helper.mjs +14 -4
  18. package/lib/src/common/project-meta.mjs +2 -2
  19. package/lib/src/common/template-compiler.mjs +2 -0
  20. package/lib/src/common/transpile-ts.mjs +94 -0
  21. package/lib/src/create-app/context-builder.mjs +1 -0
  22. package/lib/src/create-app/git-manager.mjs +12 -2
  23. package/lib/src/create-app/json-manager.mjs +21 -18
  24. package/lib/src/create-app/new-app-prompt.mjs +4 -2
  25. package/lib/src/create-app/success-message.mjs +1 -1
  26. package/lib/src/update-app/conflict-report-writer.mjs +33 -21
  27. package/lib/src/update-app/json-drift.mjs +54 -0
  28. package/lib/src/update-app/project-updater.mjs +123 -14
  29. package/lib/src/update-app/template-differ.mjs +5 -5
  30. package/lib/src/update-app/three-way-merge.mjs +42 -0
  31. package/lib/src/update-app/update-context-builder.mjs +1 -0
  32. package/lib/src/update-app/update-summary-printer.mjs +10 -2
  33. package/package.json +12 -11
  34. package/templates/base/.gitignore.hbs +1 -0
  35. package/templates/base/AGENTS.md.hbs +45 -43
  36. package/templates/base/GETTING_STARTED.md.hbs +32 -23
  37. package/templates/base/README.md.hbs +7 -4
  38. package/templates/base/nutin.config.js.hbs +1 -1
  39. package/templates/base/src/app/components/.gitkeep +0 -0
  40. package/templates/base/src/app/globals.d.ts.hbs +6 -3
  41. package/templates/base/src/app/guards.ts.hbs +1 -3
  42. package/templates/base/src/app/main.ts.hbs +2 -9
  43. package/templates/base/src/app/routes.ts.hbs +9 -3
  44. package/templates/base/src/app/services/.gitkeep +0 -0
  45. package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
  46. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  47. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
  48. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
  49. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
  50. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
  51. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
  52. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +307 -21
  53. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
  54. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +105 -4
  55. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +136 -62
  56. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
  57. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +101 -51
  58. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
  59. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
  60. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
  61. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
  62. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
  63. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
  64. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
  65. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
  66. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
  67. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
  68. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
  69. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
  70. package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
  71. package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
  72. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
  73. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
  74. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
  75. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
  76. package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
  77. package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
  78. package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
  79. package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
  80. package/templates/base/src/core/index.ts.hbs +4 -2
  81. package/templates/base/src/core/internals.d.ts.hbs +199 -20
  82. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
  83. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
  84. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
  85. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
  86. package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
  87. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
  88. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
  89. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
  90. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
  91. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
  92. package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
  93. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
  94. package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
  95. package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
  96. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
  97. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
  98. package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
  99. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +217 -4
  100. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +210 -65
  101. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
  102. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
  103. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
  104. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
  105. package/templates/base/src/core/services/router/router.test.js.hbs +321 -0
  106. package/templates/base/src/core/services/router/router.ts.hbs +122 -54
  107. package/templates/base/src/index.html.hbs +1 -0
  108. package/templates/base/src/styles/_styles.scss.hbs +1 -113
  109. package/templates/base/src/styles/main.scss.hbs +1 -1
  110. package/templates/base/tools/builder/builder.config.js.hbs +6 -0
  111. package/templates/base/tools/builder/builder.js.hbs +26 -8
  112. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
  113. package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
  114. package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
  115. package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
  116. package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
  117. package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
  118. package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
  119. package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
  120. package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
  121. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
  122. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
  123. package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
  124. package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
  125. package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
  126. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
  127. package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
  128. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
  129. package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
  130. package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
  131. package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
  132. package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
  133. package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
  134. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
  135. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +25 -8
  136. package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
  137. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
  138. package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
  139. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
  140. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +61 -15
  141. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
  142. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
  143. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +48 -18
  144. package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
  145. package/templates/base/tools/builder/core/seo/route-output.js.hbs +26 -8
  146. package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
  147. package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
  148. package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
  149. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
  150. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
  151. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
  152. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +26 -9
  153. package/templates/base/tools/builder/core/styles/sass.js.hbs +7 -5
  154. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
  155. package/templates/base/tools/dev/dev-serve.js.hbs +71 -7
  156. package/templates/base/tools/dev/serve.js.hbs +152 -22
  157. package/templates/base/tools/dev/watcher.js.hbs +75 -28
  158. package/templates/base/tools/generator/generator.js.hbs +23 -18
  159. package/templates/base/tools/generator/handle-file.js.hbs +4 -24
  160. package/templates/base/tools/generator/handle-file.test.js.hbs +4 -23
  161. package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
  162. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
  163. package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
  164. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
  165. package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
  166. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  167. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
  168. package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
  169. package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
  170. package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
  171. package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
  172. package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
  173. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
  174. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
  175. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
  176. package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
  177. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
  178. package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
  179. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
  180. package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
  181. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
  182. package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
  183. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
  184. package/templates/base/tools/utils/build-lock.js.hbs +97 -0
  185. package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
  186. package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
  187. package/templates/base/tools/utils/format.js.hbs +1 -1
  188. package/templates/base/tools/utils/index.js.hbs +3 -1
  189. package/templates/base/tools/utils/print.js.hbs +2 -2
  190. package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
  191. package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
  192. package/templates/base/tools/utils/run-command.js.hbs +30 -1
  193. package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
  194. package/templates/base/tools/utils/run-script.js.hbs +2 -2
  195. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +5 -2
  196. package/templates/features/docker/tools/docker/nginx.conf.template +19 -2
  197. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
  198. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
  199. package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
  200. package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
  201. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
  202. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
  203. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
  204. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
  205. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +29 -0
  206. package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
  207. package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
  208. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
  209. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
  210. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
  211. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
  212. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
  213. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
  214. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
  215. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
  216. package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
  217. package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
  218. package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
  219. package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
  220. package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
  221. package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
  222. package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
  223. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
  224. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
  225. package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
  226. package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
  227. package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
  228. package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
  229. package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
  230. package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
  231. package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
  232. package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
  233. package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
  234. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
  235. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
  236. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
  237. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
  238. package/templates/base/src/app/components/index.ts.hbs +0 -1
  239. package/templates/base/src/app/services/index.ts.hbs +0 -1
  240. package/templates/base/src/app/views/index.ts.hbs +0 -3
  241. /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
@@ -1,57 +1,78 @@
1
1
  import { MockI18n } from './mock-i18n.js';
2
2
 
3
3
  describe('MockI18n', () => {
4
- it('exposes the constructor defaults via currentLanguage', () => {
4
+ it('exposes the constructor defaults via the language getters', () => {
5
5
  const i18n = new MockI18n('fr', ['en', 'fr']);
6
+
6
7
  expect(i18n.currentLanguage).toBe('fr');
8
+ expect(i18n.defaultLanguage).toBe('fr');
9
+ expect(i18n.languages).toEqual(['en', 'fr']);
7
10
  });
8
11
 
9
- it('translate/getBrowserLanguage run their built-in default implementation without needing .mockImplementation', () => {
12
+ it('translate() falls back to textContent, then to the key itself', () => {
10
13
  const i18n = new MockI18n();
11
14
 
12
- // No translations set — translate() falls back to returning the key itself.
15
+ expect(i18n.translate('some.key', 'Fallback text')).toBe('Fallback text');
13
16
  expect(i18n.translate('some.key')).toBe('some.key');
14
- expect(i18n.getBrowserLanguage()).toBe('en');
15
17
  });
16
18
 
17
- it('loadTranslations/initTranslations run their default implementation and update currentLanguage', async () => {
18
- const i18n = new MockI18n('en', ['en', 'fr']);
19
+ it('translate() resolves nested dot keys from setTranslations()', () => {
20
+ const i18n = new MockI18n();
21
+ i18n.setTranslations({ home: { title: 'Home' } });
19
22
 
23
+ expect(i18n.translate('home.title')).toBe('Home');
24
+ });
25
+
26
+ it('translate() falls back to the default-language translations off the default language', async () => {
27
+ const i18n = new MockI18n('en', ['en', 'fr']);
28
+ i18n.setDefaultTranslations({ greeting: 'Hello' });
20
29
  await i18n.loadTranslations('fr');
21
- expect(i18n.currentLanguage).toBe('fr');
30
+
31
+ expect(i18n.translate('greeting')).toBe('Hello');
22
32
  });
23
33
 
24
- it('translate is still trackable as a mock alongside its default implementation', () => {
34
+ it('getTranslationObject() returns the nested value, or null', () => {
25
35
  const i18n = new MockI18n();
26
- i18n.translate('some.key');
36
+ i18n.setTranslations({ nav: { home: 'Home', about: 'About' } });
27
37
 
28
- expect(i18n.translate).toHaveBeenCalledWith('some.key');
38
+ expect(i18n.getTranslationObject('nav')).toEqual({ home: 'Home', about: 'About' });
39
+ expect(i18n.getTranslationObject('missing')).toBe(null);
29
40
  });
30
41
 
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
- });
42
+ it('setCurrentLanguage() updates currentLanguage and notifies onLanguageChange() callbacks', async () => {
43
+ const i18n = new MockI18n('en', ['en', 'fr']);
44
+ let payload;
45
+ i18n.onLanguageChange((data) => { payload = data; });
39
46
 
40
- expect(i18n.translate('greeting', { name: 'Ada' })).toBe('Hello Ada');
47
+ await i18n.setCurrentLanguage('fr');
48
+
49
+ expect(i18n.currentLanguage).toBe('fr');
50
+ expect(payload).toEqual({ lang: 'fr' });
51
+ expect(i18n.setCurrentLanguage).toHaveBeenCalledWith('fr');
52
+ });
53
+
54
+ it('onLanguageChange() returns a function that stops further notifications', async () => {
55
+ const i18n = new MockI18n('en', ['en', 'fr']);
56
+ let calls = 0;
57
+ const unsubscribe = i18n.onLanguageChange(() => { calls++; });
58
+ unsubscribe();
59
+
60
+ await i18n.setCurrentLanguage('fr');
61
+
62
+ expect(calls).toBe(0);
41
63
  });
42
64
 
43
- it('setTranslations/setDefaultTranslations seed real, readable state', () => {
65
+ it('translate is still trackable as a mock alongside its default implementation', () => {
44
66
  const i18n = new MockI18n();
45
- i18n.setTranslations({ a: '1' });
46
- i18n.setDefaultTranslations({ b: '2' });
67
+ i18n.translate('some.key');
47
68
 
48
- expect(i18n._translations).toEqual({ a: '1' });
49
- expect(i18n._defaultTranslations).toEqual({ b: '2' });
69
+ expect(i18n.translate).toHaveBeenCalledWith('some.key');
50
70
  });
51
71
 
52
- it('reset() restores the constructor default language and clears translations', () => {
53
- const i18n = new MockI18n('en');
72
+ it('reset() restores the constructor default language and clears translations', async () => {
73
+ const i18n = new MockI18n('en', ['en', 'fr']);
54
74
  i18n.setTranslations({ a: '1' });
75
+ await i18n.setCurrentLanguage('fr');
55
76
 
56
77
  i18n.reset();
57
78
 
@@ -1,23 +1,31 @@
1
1
  import { createMockMethod } from './create-mock-method.js';
2
2
 
3
+ // Mirrors Router's public API. Route params are seeded with setParams() and read back
4
+ // through the real getCurrentParams()/getParam() shapes.
3
5
  export class MockRouter {
4
6
  constructor(routes = {}) {
5
7
  this.routes = routes;
6
- this._currentView = null;
8
+ this._currentParams = {};
7
9
 
8
- this.navigate = createMockMethod();
9
- this.handlePopState = createMockMethod();
10
- this.handleNotFound = createMockMethod();
11
- this.handleGuards = createMockMethod();
12
- this.initializeEventListeners = createMockMethod();
10
+ this.navigate = createMockMethod(async () => {});
11
+ this.reload = createMockMethod(async () => {});
12
+ this.getCurrentParams = createMockMethod(() => ({ ...this._currentParams }));
13
+ this.getParam = createMockMethod((key) => this._currentParams[key]);
14
+ this.removeEventListeners = createMockMethod();
15
+ this.onDestroy = createMockMethod();
16
+ }
17
+
18
+ setParams(params) {
19
+ this._currentParams = { ...params };
13
20
  }
14
21
 
15
22
  reset() {
16
23
  this.navigate.mockReset();
17
- this.handlePopState.mockReset();
18
- this.handleNotFound.mockReset();
19
- this.handleGuards.mockReset();
20
- this.initializeEventListeners.mockReset();
21
- this._currentView = null;
24
+ this.reload.mockReset();
25
+ this.getCurrentParams.mockReset();
26
+ this.getParam.mockReset();
27
+ this.removeEventListeners.mockReset();
28
+ this.onDestroy.mockReset();
29
+ this._currentParams = {};
22
30
  }
23
31
  }
@@ -8,22 +8,39 @@ describe('MockRouter', () => {
8
8
  expect(router.routes).toBe(routes);
9
9
  });
10
10
 
11
- it('navigate/handlePopState/handleNotFound/handleGuards/initializeEventListeners are independent bare mocks', () => {
11
+ it('navigate/reload are independent trackable mocks', async () => {
12
12
  const router = new MockRouter();
13
- router.navigate('/about');
13
+ await router.navigate('/about');
14
14
 
15
15
  expect(router.navigate.calls).toEqual([['/about']]);
16
- expect(router.handlePopState.calls).toEqual([]);
16
+ expect(router.reload.calls).toEqual([]);
17
17
  });
18
18
 
19
- it('reset() clears every mock method and _currentView', () => {
19
+ it('getCurrentParams/getParam read the params seeded with setParams()', () => {
20
20
  const router = new MockRouter();
21
- router.navigate('/about');
22
- router._currentView = 'about-view';
21
+ router.setParams({ id: '42' });
22
+
23
+ expect(router.getCurrentParams()).toEqual({ id: '42' });
24
+ expect(router.getParam('id')).toBe('42');
25
+ expect(router.getParam('missing')).toBe(undefined);
26
+ });
27
+
28
+ it('getCurrentParams returns a copy', () => {
29
+ const router = new MockRouter();
30
+ router.setParams({ id: '42' });
31
+ router.getCurrentParams().id = 'changed';
32
+
33
+ expect(router.getParam('id')).toBe('42');
34
+ });
35
+
36
+ it('reset() clears every mock method and the seeded params', async () => {
37
+ const router = new MockRouter();
38
+ await router.navigate('/about');
39
+ router.setParams({ id: '42' });
23
40
 
24
41
  router.reset();
25
42
 
26
43
  expect(router.navigate.calls).toEqual([]);
27
- expect(router._currentView).toBe(null);
44
+ expect(router.getCurrentParams()).toEqual({});
28
45
  });
29
46
  });
@@ -12,7 +12,9 @@ const verboseEnabled = args.includes('--verbose');
12
12
  const fileFilters = args.filter(arg => arg !== '--coverage' && arg !== '--verbose');
13
13
 
14
14
  // Tests import and run the compiled build output (dist/src/)
15
+ {{#unless (eq lang "js")}}
15
16
  // and has no sourcemap back to the .ts sources
17
+ {{/unless}}
16
18
  const origins = [];
17
19
  const coverageDirs = [];
18
20
 
@@ -41,10 +43,8 @@ if (fileFilters.length) {
41
43
  }
42
44
 
43
45
  async function loadTestFile(file) {
44
- setupJsdom();
45
46
  setCurrentTestFile(file);
46
47
  await import(file);
47
- teardownJsdom();
48
48
  }
49
49
 
50
50
  async function runCoverage(testResults) {
@@ -77,18 +77,27 @@ async function runTests() {
77
77
 
78
78
  if (coverageEnabled) await startCoverage();
79
79
 
80
+ // One DOM for importing every file (module-level code may use it); each suite
81
+ // then runs in a fresh one (see runQueuedTests).
82
+ let loadFailures = 0;
83
+ setupJsdom();
80
84
  for (const file of testFiles) {
81
85
  try {
82
86
  await loadTestFile(file);
83
87
  } catch (error) {
88
+ loadFailures++;
84
89
  print.boldError(`✗ ${file} failed to load:`);
85
90
  print.grayError(error.stack);
86
91
  }
87
92
  }
93
+ teardownJsdom();
88
94
 
89
95
  const testResults = await runQueuedTests();
90
96
 
91
- if (testResults.failed > 0) process.exitCode = 1;
97
+ if (loadFailures > 0) {
98
+ print.boldError(`✗ ${loadFailures} test file(s) failed to load — see errors above.\n`);
99
+ }
100
+ if (testResults.failed > 0 || loadFailures > 0) process.exitCode = 1;
92
101
 
93
102
  if (coverageEnabled) await runCoverage(testResults);
94
103
  }
@@ -1,37 +1,62 @@
1
1
  import chokidar from 'chokidar';
2
2
  import { exec } from 'child_process';
3
3
  import path from 'path';
4
- import { print } from './core/index.js'
4
+ import { print } from './core/index.js';
5
+ import config from '#root/nutin.config.js';
5
6
 
6
- const watcher = chokidar.watch(['src', 'test', 'unit', 'e2e'], {
7
+ // Tests import the build output (dist/), so every run rebuilds first.
8
+ const COMMAND = '{{packageManager}} run testin-nutin';
9
+
10
+ const watched = ['src'];
11
+ if (config.testinNutin.includeTools) watched.push('tools');
12
+
13
+ const watcher = chokidar.watch(watched, {
7
14
  ignored: /(^|[/\\])\../,
8
15
  persistent: true,
16
+ ignoreInitial: true,
9
17
  });
10
18
 
11
- print.boldHead('Watching for test changes...')
19
+ let isRunning = false;
20
+ let pendingRun = false;
21
+ let runTimeout = null;
22
+ let lastChangedPath = null;
23
+
24
+ function runTests() {
25
+ isRunning = true;
26
+ pendingRun = false;
27
+
28
+ console.clear();
29
+ print.boldInfo(`\n🔄 File changed: ${path.relative(process.cwd(), lastChangedPath)}\n`);
30
+
31
+ exec(COMMAND, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
32
+ if (stdout) process.stdout.write(stdout);
33
+ if (stderr) process.stderr.write(stderr);
34
+ if (err) print.boldError(`\nTest run failed: ${err.message}`);
35
+ print.boldHead('Watching for test changes...');
36
+ isRunning = false;
37
+
38
+ // A change arrived mid-run — run again instead of dropping it.
39
+ if (pendingRun) runTests();
40
+ });
41
+ }
12
42
 
13
43
  watcher.on('error', (err) => {
14
44
  print.boldError(`\nWatcher error: ${err.message}`);
15
45
  });
16
46
 
17
- let isRunning = false;
18
- let runTimeout = null;
47
+ watcher.on('all', (event, filePath) => {
48
+ if (!['add', 'change', 'unlink'].includes(event)) return;
49
+ lastChangedPath = filePath;
19
50
 
20
- watcher.on('change', (filePath) => {
21
- if (runTimeout) clearTimeout(runTimeout);
51
+ if (isRunning) {
52
+ pendingRun = true;
53
+ return;
54
+ }
22
55
 
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`);
56
+ if (runTimeout) clearTimeout(runTimeout);
57
+ runTimeout = setTimeout(runTests, 100);
58
+ });
29
59
 
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);
60
+ watcher.on('ready', () => {
61
+ print.boldHead('Watching for test changes...');
37
62
  });
@@ -0,0 +1,97 @@
1
+ import * as fs from 'fs';
2
+ import path from 'path';
3
+ import { print } from './print.js';
4
+
5
+ // Guards the whole build pipeline against concurrent invocations — e.g. a manual
6
+ // `{{packageManager}} run build` running alongside the dev watcher's auto-rebuild, or (as actually
7
+ // happened) multiple orphaned watcher.js processes each kicking off their own build.
8
+ // copy-static.js starts every run by wiping and recreating dist-build; two builds
9
+ // doing that at once corrupt each other's output (missing/non-empty dist-build,
10
+ // merge-templates.js "has both an inline and external template" conflicts, etc).
11
+ const LOCK_DIR = path.resolve('.build-lock');
12
+ const STALE_MS = 5 * 60 * 1000; // a full build takes seconds — anything older is a dead leftover
13
+ const POLL_MS = 200;
14
+ const MAX_WAIT_MS = 2 * 60 * 1000;
15
+
16
+ function tryAcquire() {
17
+ try {
18
+ fs.mkdirSync(LOCK_DIR);
19
+ fs.writeFileSync(path.join(LOCK_DIR, 'pid'), String(process.pid));
20
+ return true;
21
+ } catch (err) {
22
+ if (err.code === 'EEXIST') return false;
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ function isStale() {
28
+ let mtimeMs;
29
+ try {
30
+ ({ mtimeMs } = fs.statSync(LOCK_DIR));
31
+ } catch {
32
+ return true; // lock vanished mid-check (released concurrently) — treat as free
33
+ }
34
+ if (Date.now() - mtimeMs > STALE_MS) return true;
35
+ return isHolderDead();
36
+ }
37
+
38
+ // A build killed hard (kill -9, a crash) never releases — free the lock right away
39
+ // when the pid that wrote it is gone instead of waiting out STALE_MS.
40
+ function isHolderDead() {
41
+ let pid;
42
+ try {
43
+ pid = Number(fs.readFileSync(path.join(LOCK_DIR, 'pid'), 'utf-8'));
44
+ } catch {
45
+ return false; // pid not written yet (holder is mid-acquire) — not provably dead
46
+ }
47
+ if (!Number.isInteger(pid) || pid <= 0) return false;
48
+ try {
49
+ process.kill(pid, 0);
50
+ return false;
51
+ } catch (err) {
52
+ return err.code === 'ESRCH';
53
+ }
54
+ }
55
+
56
+ function release() {
57
+ try {
58
+ fs.rmSync(LOCK_DIR, { recursive: true, force: true });
59
+ } catch {
60
+ // already released
61
+ }
62
+ }
63
+
64
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
65
+
66
+ export async function acquireBuildLock() {
67
+ const start = Date.now();
68
+ let warned = false;
69
+
70
+ while (!tryAcquire()) {
71
+ if (isStale()) {
72
+ release();
73
+ continue;
74
+ }
75
+
76
+ if (!warned) {
77
+ print.boldInfo('\nAnother build is already in progress — waiting for it to finish...');
78
+ warned = true;
79
+ }
80
+
81
+ if (Date.now() - start > MAX_WAIT_MS) {
82
+ throw new Error(`Timed out waiting for the build lock (${LOCK_DIR}) held by another process.`);
83
+ }
84
+
85
+ await sleep(POLL_MS);
86
+ }
87
+
88
+ process.on('exit', release);
89
+ // 'exit' doesn't fire when a signal terminates the process (Ctrl-C, dev-serve's
90
+ // group kill), so release explicitly there too, then exit the way the signal would have.
91
+ for (const [signal, code] of [['SIGINT', 130], ['SIGTERM', 143], ['SIGHUP', 129]]) {
92
+ process.once(signal, () => {
93
+ release();
94
+ process.exit(code);
95
+ });
96
+ }
97
+ }
@@ -0,0 +1,72 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { print } from './print.js';
4
+ import { errorExit } from './error-exit.js';
5
+ import { runCommand } from './run-command.js';
6
+ import { promptBoolean } from './prompt-boolean.js';
7
+
8
+ export function isInstalled(depName) {
9
+ return fs.existsSync(path.join(process.cwd(), 'node_modules', depName, 'package.json'));
10
+ }
11
+
12
+ export function detectPackageManager() {
13
+ const has = (file) => fs.existsSync(path.join(process.cwd(), file));
14
+ if (has('pnpm-lock.yaml')) return 'pnpm';
15
+ if (has('yarn.lock')) return 'yarn';
16
+ if (has('bun.lock') || has('bun.lockb')) return 'bun';
17
+ return 'npm';
18
+ }
19
+
20
+ export function getDevInstallArgs(packageManager, deps) {
21
+ const pkgList = deps.map((dep) => `${dep.name}@${dep.version}`);
22
+ return packageManager === 'npm'
23
+ ? ['npm', ['install', '-D', ...pkgList]]
24
+ : [packageManager, ['add', '-D', ...pkgList]];
25
+ }
26
+
27
+ // builder.js forwards -y/--yes through the env, since runScript doesn't forward argv.
28
+ function assumeYes() {
29
+ return process.env.NUTIN_ASSUME_YES === '1' || process.argv.includes('-y') || process.argv.includes('--yes');
30
+ }
31
+
32
+ /**
33
+ * Makes sure an opt-in feature's devDependencies are installed before its build step
34
+ * imports them. Prod builds never install. Dev builds ask on a TTY; elsewhere (CI, the
35
+ * dev watcher) they only install with -y/--yes, never silently.
36
+ */
37
+ export async function ensureDeps({ feature, deps, isProd, origin }) {
38
+ const missing = deps.filter((dep) => !isInstalled(dep.name));
39
+ if (!missing.length) return;
40
+
41
+ const [command, args] = getDevInstallArgs(detectPackageManager(), missing);
42
+ const installCommand = [command, ...args].join(' ');
43
+
44
+ if (isProd) {
45
+ errorExit(`${feature} is enabled but its dependencies are missing.\nRun "${installCommand}" before rebuilding.`, origin);
46
+ }
47
+
48
+ print.warn(`${feature} is enabled but its dependencies are missing.`);
49
+ print.gray('Required packages:');
50
+ missing.forEach((dep) => print.gray(` - ${dep.name}@${dep.version}`));
51
+
52
+ let shouldInstall;
53
+ if (assumeYes()) {
54
+ print.gray(`-y/--yes passed. Installing automatically: ${installCommand}`);
55
+ shouldInstall = true;
56
+ } else if (process.stdin.isTTY) {
57
+ shouldInstall = await promptBoolean('Install them now?');
58
+ } else {
59
+ errorExit(`Non-interactive shell detected. Run "${installCommand}", or rebuild with "-- -y" to install automatically.`, origin);
60
+ }
61
+
62
+ if (!shouldInstall) {
63
+ print.boldError(`Aborting. Run manually: ${installCommand}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ try {
68
+ await runCommand(command, args);
69
+ } catch (err) {
70
+ errorExit(err, `${origin}-install`);
71
+ }
72
+ }
@@ -0,0 +1,17 @@
1
+ import { isInstalled, getDevInstallArgs } from './ensure-deps.js';
2
+
3
+ describe('ensure-deps', () => {
4
+ it('getDevInstallArgs uses "install -D" for npm and "add -D" for the others', () => {
5
+ const deps = [{ name: 'a', version: '^1.0.0' }, { name: '@scope/b', version: '^2.0.0' }];
6
+
7
+ expect(getDevInstallArgs('npm', deps)).toEqual(['npm', ['install', '-D', 'a@^1.0.0', '@scope/b@^2.0.0']]);
8
+ expect(getDevInstallArgs('pnpm', deps)).toEqual(['pnpm', ['add', '-D', 'a@^1.0.0', '@scope/b@^2.0.0']]);
9
+ expect(getDevInstallArgs('yarn', deps)[1][0]).toBe('add');
10
+ expect(getDevInstallArgs('bun', deps)[1][0]).toBe('add');
11
+ });
12
+
13
+ it('isInstalled checks node_modules/<name>/package.json', () => {
14
+ expect(isInstalled('esbuild')).toBeTruthy();
15
+ expect(isInstalled('surely-not-an-installed-package-xyz')).toBeFalsy();
16
+ });
17
+ });
@@ -8,7 +8,7 @@ export function capitalized(input) {
8
8
  const formatted = input.replace(
9
9
  /\w*/g,
10
10
  function(txt) {
11
- return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
11
+ return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase();
12
12
  })
13
13
  return formatted.replace(/,/g, '');
14
14
  }
@@ -4,5 +4,7 @@ export * from './get-files-recursive.js';
4
4
  export * from './run-command.js';
5
5
  export * from './run-script.js';
6
6
  export * from './prompt-boolean.js';
7
- export * from './languages.js';
8
7
  export * from './error-exit.js';
8
+ export * from './build-lock.js';
9
+ export * from './resolve-port.js';
10
+ export * from './ensure-deps.js';
@@ -16,7 +16,7 @@ export const chalk = {
16
16
  };
17
17
 
18
18
  export const print = {
19
- head: (s) => console.info(`${chalk.purple(s)}`),
19
+ head: (s) => console.info(`${chalk.purple(s)}`),
20
20
  boldHead : (s) => console.info(`${chalk.boldPurple(s)}`),
21
21
  section: (s) => console.info(`${chalk.yellow(s)}`),
22
22
  boldSection: (s) => console.info(`${chalk.boldYellow(s)}`),
@@ -32,6 +32,6 @@ export const print = {
32
32
  boldError: (s) => console.error(`${chalk.boldRed(s)}`),
33
33
  grayError: (s) => console.error(`${chalk.boldGray(s)}`),
34
34
  warn: (s) => console.warn(`${chalk.yellow(s)}`),
35
- boldWarn: (s) => console.warn(`${chalk.yellow(s)}`),
35
+ boldWarn: (s) => console.warn(`${chalk.boldYellow(s)}`),
36
36
  debug: (s) => console.debug(`${chalk.gray(s)}`),
37
37
  }
@@ -0,0 +1,30 @@
1
+ export const DEFAULT_PORT = 9090;
2
+
3
+ // Precedence: --port flag > npm_config_port > PORT env > DEFAULT_PORT.
4
+ // npm_config_port covers older npm versions turning `npm run dev --port=3000`
5
+ // (no `--`) into config instead of argv; npm >= 11 rejects that with EUNKNOWNCONFIG.
6
+ export function resolvePort(argv = process.argv.slice(2), env = process.env) {
7
+ const raw = readPortFlag(argv) ?? env.npm_config_port ?? env.PORT;
8
+ if (raw === undefined || raw === '') return DEFAULT_PORT;
9
+
10
+ const port = Number(raw);
11
+ if (!/^\d+$/.test(String(raw)) || port < 1 || port > 65535) {
12
+ throw new Error(`Invalid port "${raw}" — expected an integer between 1 and 65535.`);
13
+ }
14
+ return port;
15
+ }
16
+
17
+ function readPortFlag(argv) {
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const arg = argv[i];
20
+ if (arg.startsWith('--port=')) return arg.slice('--port='.length);
21
+ if (arg === '--port') {
22
+ const next = argv[i + 1];
23
+ if (next === undefined || next.startsWith('--')) {
24
+ throw new Error('Missing value for --port (e.g. --port 3000).');
25
+ }
26
+ return next;
27
+ }
28
+ }
29
+ return undefined;
30
+ }
@@ -0,0 +1,47 @@
1
+ import { resolvePort, DEFAULT_PORT } from './resolve-port.js';
2
+
3
+ describe('resolvePort', () => {
4
+ it('defaults to 9090 with no flag or env', () => {
5
+ expect(resolvePort([], {})).toBe(DEFAULT_PORT);
6
+ expect(DEFAULT_PORT).toBe(9090);
7
+ });
8
+
9
+ it('reads --port <n>', () => {
10
+ expect(resolvePort(['--port', '3000'], {})).toBe(3000);
11
+ });
12
+
13
+ it('reads --port=<n>', () => {
14
+ expect(resolvePort(['--silent', '--port=3001'], {})).toBe(3001);
15
+ });
16
+
17
+ it('reads npm_config_port (npm rewrites --port into config)', () => {
18
+ expect(resolvePort([], { npm_config_port: '3002' })).toBe(3002);
19
+ });
20
+
21
+ it('reads PORT', () => {
22
+ expect(resolvePort([], { PORT: '3003' })).toBe(3003);
23
+ });
24
+
25
+ it('prefers --port over npm_config_port over PORT', () => {
26
+ expect(resolvePort(['--port', '1111'], { npm_config_port: '2222', PORT: '3333' })).toBe(1111);
27
+ expect(resolvePort([], { npm_config_port: '2222', PORT: '3333' })).toBe(2222);
28
+ });
29
+
30
+ it('ignores an empty PORT', () => {
31
+ expect(resolvePort([], { PORT: '' })).toBe(DEFAULT_PORT);
32
+ });
33
+
34
+ it('throws on a non-numeric port', () => {
35
+ expect(() => resolvePort(['--port', 'abc'], {})).toThrow('Invalid port "abc"');
36
+ });
37
+
38
+ it('throws on out-of-range ports', () => {
39
+ expect(() => resolvePort(['--port=0'], {})).toThrow('Invalid port "0"');
40
+ expect(() => resolvePort([], { PORT: '70000' })).toThrow('Invalid port "70000"');
41
+ });
42
+
43
+ it('throws when --port has no value', () => {
44
+ expect(() => resolvePort(['--port'], {})).toThrow('Missing value for --port');
45
+ expect(() => resolvePort(['--port', '--silent'], {})).toThrow('Missing value for --port');
46
+ });
47
+ });