@nutin/cli 2.1.1 → 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 (229) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/README.md +13 -5
  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 +9 -8
  34. package/templates/base/AGENTS.md.hbs +42 -41
  35. package/templates/base/GETTING_STARTED.md.hbs +26 -20
  36. package/templates/base/README.md.hbs +7 -4
  37. package/templates/base/src/app/globals.d.ts.hbs +6 -3
  38. package/templates/base/src/app/guards.ts.hbs +1 -3
  39. package/templates/base/src/app/main.ts.hbs +2 -9
  40. package/templates/base/src/app/routes.ts.hbs +7 -2
  41. package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
  42. package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
  43. package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
  44. package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
  45. package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
  46. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
  47. package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
  48. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +286 -24
  49. package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
  50. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +66 -4
  51. package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
  52. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
  53. package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +93 -54
  54. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
  55. package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
  56. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
  57. package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
  58. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
  59. package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
  60. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
  61. package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
  62. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
  63. package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
  64. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
  65. package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
  66. package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
  67. package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
  68. package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
  69. package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
  70. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
  71. package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
  72. package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
  73. package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
  74. package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
  75. package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
  76. package/templates/base/src/core/index.ts.hbs +4 -2
  77. package/templates/base/src/core/internals.d.ts.hbs +199 -20
  78. package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
  79. package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
  80. package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
  81. package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
  82. package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
  83. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
  84. package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
  85. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
  86. package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
  87. package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
  88. package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
  89. package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
  90. package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
  91. package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
  92. package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
  93. package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
  94. package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
  95. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +183 -4
  96. package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
  97. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
  98. package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
  99. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
  100. package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
  101. package/templates/base/src/core/services/router/router.test.js.hbs +306 -0
  102. package/templates/base/src/core/services/router/router.ts.hbs +113 -53
  103. package/templates/base/tools/builder/builder.config.js.hbs +6 -0
  104. package/templates/base/tools/builder/builder.js.hbs +18 -6
  105. package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
  106. package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
  107. package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
  108. package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
  109. package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
  110. package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
  111. package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
  112. package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
  113. package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
  114. package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
  115. package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
  116. package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
  117. package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
  118. package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
  119. package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
  120. package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
  121. package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
  122. package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
  123. package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
  124. package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
  125. package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
  126. package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +15 -7
  127. package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
  128. package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
  129. package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
  130. package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
  131. package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +43 -13
  132. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
  133. package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
  134. package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +28 -21
  135. package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
  136. package/templates/base/tools/builder/core/seo/route-output.js.hbs +24 -6
  137. package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
  138. package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
  139. package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
  140. package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
  141. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
  142. package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
  143. package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +24 -11
  144. package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
  145. package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
  146. package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
  147. package/templates/base/tools/dev/serve.js.hbs +152 -22
  148. package/templates/base/tools/dev/watcher.js.hbs +34 -3
  149. package/templates/base/tools/generator/generator.js.hbs +1 -1
  150. package/templates/base/tools/generator/handle-file.js.hbs +4 -2
  151. package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
  152. package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
  153. package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
  154. package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
  155. package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
  156. package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
  157. package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
  158. package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
  159. package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
  160. package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
  161. package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
  162. package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
  163. package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
  164. package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
  165. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
  166. package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
  167. package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
  168. package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
  169. package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
  170. package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
  171. package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
  172. package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
  173. package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
  174. package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
  175. package/templates/base/tools/utils/build-lock.js.hbs +30 -2
  176. package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
  177. package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
  178. package/templates/base/tools/utils/format.js.hbs +1 -1
  179. package/templates/base/tools/utils/index.js.hbs +2 -1
  180. package/templates/base/tools/utils/print.js.hbs +2 -2
  181. package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
  182. package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
  183. package/templates/base/tools/utils/run-command.js.hbs +30 -1
  184. package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
  185. package/templates/base/tools/utils/run-script.js.hbs +2 -2
  186. package/templates/features/docker/tools/docker/Dockerfile.template.hbs +3 -0
  187. package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
  188. package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
  189. package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
  190. package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
  191. package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
  192. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
  193. package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
  194. package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
  195. package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
  196. package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +21 -4
  197. package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
  198. package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
  199. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
  200. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
  201. package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
  202. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
  203. package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
  204. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
  205. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
  206. package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
  207. package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
  208. package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
  209. package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
  210. package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
  211. package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
  212. package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
  213. package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
  214. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
  215. package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
  216. package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
  217. package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
  218. package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
  219. package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
  220. package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
  221. package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
  222. package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
  223. package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
  224. package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
  225. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
  226. package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
  227. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
  228. package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
  229. /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
@@ -1,77 +1,37 @@
1
1
  import { execFileSync } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import path from 'path';
4
- import { print, runCommand, promptBoolean, errorExit } from '../../../utils/index.js';
4
+ import { ensureDeps, errorExit } from '../../../utils/index.js';
5
5
  import { PATHS } from '../app/paths.js';
6
6
  import { builderConfig } from '../../builder.config.js';
7
7
 
8
- const REQUIRED_DEPS = [
9
- { name: 'tailwindcss', version: '^4.3.0' },
10
- { name: '@tailwindcss/cli', version: '^4.3.0' },
11
- ];
12
-
13
- const isInstalled = (depName) => fs.existsSync(path.join(process.cwd(), 'node_modules', depName, 'package.json'));
14
-
15
- const getPackageManager = () => {
16
- if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
17
- if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
18
- if (fs.existsSync(path.join(process.cwd(), 'bun.lock')) || fs.existsSync(path.join(process.cwd(), 'bun.lockb'))) return 'bun';
19
- return 'npm';
20
- };
21
-
22
- const getInstallCommand = (packageManager, deps) => {
23
- const pkgList = deps.map(dep => `${dep.name}@${dep.version}`).join(' ');
24
- switch (packageManager) {
25
- case 'yarn': return `yarn add -D ${pkgList}`;
26
- case 'pnpm': return `pnpm add -D ${pkgList}`;
27
- case 'bun': return `bun add -D ${pkgList}`;
28
- default: return `npm install -D ${pkgList}`;
29
- }
30
- };
31
-
32
- const missingDeps = REQUIRED_DEPS.filter(dep => !isInstalled(dep.name));
33
-
34
- if (missingDeps.length) {
35
- if (builderConfig.isProd) {
36
- errorExit('Tailwind CSS is enabled (tailwind: true) but its dependencies are missing.\nRun "npm install" before rebuilding');
37
- }
38
- const packageManager = getPackageManager();
39
- const installCommand = getInstallCommand(packageManager, missingDeps);
40
-
41
- print.warn('Tailwind CSS is enabled (tailwind: true) but its dependencies are missing.');
42
- print.gray('Required packages:');
43
- missingDeps.forEach(dep => print.gray(` - ${dep.name}@${dep.version}`));
44
-
45
- let shouldInstall = true;
46
- if (process.stdin.isTTY) {
47
- shouldInstall = await promptBoolean('Install them now?');
48
- } else {
49
- print.gray(`Non-interactive shell detected. Installing automatically: ${installCommand}`);
50
- }
51
-
52
- if (!shouldInstall) {
53
- print.boldError(`Aborting. Run manually: ${installCommand}`);
54
- process.exit(1);
55
- }
56
-
57
- const [command, ...args] = installCommand.split(' ');
58
- try {
59
- await runCommand(command, args);
60
- } catch (err) {
61
- errorExit(err, 'tailwind-install');
62
- }
8
+ await ensureDeps({
9
+ feature: 'Tailwind CSS (tailwind: true)',
10
+ deps: [
11
+ { name: 'tailwindcss', version: '^4.3.0' },
12
+ { name: '@tailwindcss/cli', version: '^4.3.0' },
13
+ ],
14
+ isProd: builderConfig.isProd,
15
+ origin: 'tailwind',
16
+ });
17
+
18
+ // Run the CLI's JS entry with node rather than node_modules/.bin/tailwindcss, which is a
19
+ // .cmd shim on Windows that execFileSync can't launch without a shell.
20
+ function tailwindEntry() {
21
+ const cliDir = path.join(process.cwd(), 'node_modules', '@tailwindcss', 'cli');
22
+ const { bin } = JSON.parse(fs.readFileSync(path.join(cliDir, 'package.json'), 'utf-8'));
23
+ return path.join(cliDir, typeof bin === 'string' ? bin : bin.tailwindcss);
63
24
  }
64
25
 
65
- const twBin = path.join(process.cwd(), 'node_modules', '.bin', 'tailwindcss');
66
26
  const input = path.join(PATHS.source, 'styles', 'tailwind.css');
67
27
  const output = path.join(PATHS.tempSource, 'tw-out.css');
68
28
  const mainCss = path.join(PATHS.tempSource, 'main.css');
69
29
 
70
- const args = ['-i', input, '-o', output, '--silent'];
30
+ const args = [tailwindEntry(), '-i', input, '-o', output, '--silent'];
71
31
  if (builderConfig.isProd) args.push('--minify');
72
32
 
73
33
  try {
74
- execFileSync(twBin, args, { stdio: 'inherit' });
34
+ execFileSync(process.execPath, args, { stdio: 'inherit' });
75
35
  } catch (err) {
76
36
  errorExit(err, 'tailwind');
77
37
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn, spawnSync } from 'child_process';
4
- import { print, runCommand } from '../utils/index.js';
4
+ import { print, resolvePort, runCommand } from '../utils/index.js';
5
5
 
6
6
  const isWindows = process.platform === 'win32';
7
7
 
@@ -9,6 +9,14 @@ async function startDev() {
9
9
  console.clear();
10
10
  print.blue('🚀 Starting Dev Environment...\n');
11
11
 
12
+ let port;
13
+ try {
14
+ port = resolvePort();
15
+ } catch (err) {
16
+ print.boldError(err.message);
17
+ process.exit(1);
18
+ }
19
+
12
20
  try {
13
21
  await runCommand('{{packageManager}}', ['run', 'build', '--silent']);
14
22
  } catch (err) {
@@ -25,8 +33,23 @@ async function startDev() {
25
33
  // Not on Windows: there, detached children leave the console, so Ctrl-C would no
26
34
  // longer reach them, and negative-pid group kills aren't supported — taskkill /T
27
35
  // walks the process tree instead.
28
- const serve = spawn(['{{packageManager}}', 'run', 'serve:only', '--silent'].join(' '), { stdio: 'inherit', shell: true, detached: !isWindows });
29
- const watcher = spawn(['node', 'tools/dev/watcher.js', '--silent'].join(' '), { stdio: 'inherit', shell: true, detached: !isWindows });
36
+ // Hand serve.js the already-resolved port via PORT; npm_config_port is dropped so it
37
+ // can't take precedence over it in the child.
38
+ const { npm_config_port, ...serveEnv } = process.env;
39
+ serveEnv.PORT = String(port);
40
+ // Both run node directly (no shell/npm layer) with an IPC channel, used as a relay:
41
+ // serve.js reports the URL it listens on (watcher.js reprints it after every rebuild),
42
+ // and watcher.js reports each successful rebuild (serve.js reloads open pages).
43
+ const stdio = ['inherit', 'inherit', 'inherit', 'ipc'];
44
+ const serve = spawn(process.execPath, ['tools/dev/serve.js'], { stdio, detached: !isWindows, env: serveEnv });
45
+ const watcher = spawn(process.execPath, ['tools/dev/watcher.js'], { stdio, detached: !isWindows });
46
+
47
+ serve.on('message', (msg) => {
48
+ if (msg?.type === 'server-url' && watcher.connected) watcher.send(msg);
49
+ });
50
+ watcher.on('message', (msg) => {
51
+ if (msg?.type === 'reload' && serve.connected) serve.send(msg);
52
+ });
30
53
 
31
54
  const children = [serve, watcher];
32
55
  let shuttingDown = false;
@@ -53,14 +76,16 @@ async function startDev() {
53
76
 
54
77
  process.on('SIGINT', () => shutdown('SIGINT'));
55
78
  process.on('SIGTERM', () => shutdown('SIGTERM'));
79
+ // Closing the terminal tab/window sends SIGHUP; the detached children don't get it.
80
+ process.on('SIGHUP', () => shutdown('SIGTERM'));
56
81
 
57
82
  serve.on('error', (err) => {
58
- print.boldError(`live-server failed to start: ${err.message}`);
83
+ print.boldError(`dev server failed to start: ${err.message}`);
59
84
  process.exit(1);
60
85
  });
61
86
  serve.on('close', (code) => {
62
87
  if (shuttingDown) return;
63
- print.error(`live-server exited with code ${code}`);
88
+ print.error(`dev server exited with code ${code}`);
64
89
  shutdown('SIGTERM', code ?? 1);
65
90
  });
66
91
  watcher.on('error', (err) => {
@@ -1,33 +1,163 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import liveServer from 'live-server';
4
- import { print } from '../utils/index.js';
5
-
6
- const params = {
7
- port: 9090,
8
- root: 'dist/src',
9
- open: false, // set to true to open a new window in browser
10
- logLevel: 0, // 0 = errors only, 1 = some info, 2 = verbose
11
- entryFile: 'index.html',
12
- wait: 100, // debounce reloads
13
- middleware: [
14
- function(req, res, next) {
15
- if (req.url.includes('.') && !req.url.endsWith('/')) {
16
- return next();
17
- }
18
- req.url = '/index.html';
19
- next();
20
- }
21
- ]
3
+ import http from 'http';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { print, resolvePort } from '../utils/index.js';
7
+
8
+ const ROOT = path.resolve('dist', 'src');
9
+ const RELOAD_EVENTS = '/__nutin/reload';
10
+ const RELOAD_SCRIPT = '/__nutin/reload.js';
11
+ const RELOAD_TAG = `<script src="${RELOAD_SCRIPT}"></script>`;
12
+ // External rather than inline, so a page CSP without 'unsafe-inline' doesn't block it.
13
+ const RELOAD_CLIENT = `new EventSource('${RELOAD_EVENTS}').addEventListener('reload', () => location.reload());\n`;
14
+
15
+ const MIME_TYPES = {
16
+ '.html': 'text/html; charset=utf-8',
17
+ '.js': 'text/javascript; charset=utf-8',
18
+ '.mjs': 'text/javascript; charset=utf-8',
19
+ '.css': 'text/css; charset=utf-8',
20
+ '.json': 'application/json; charset=utf-8',
21
+ '.map': 'application/json; charset=utf-8',
22
+ '.txt': 'text/plain; charset=utf-8',
23
+ '.xml': 'application/xml; charset=utf-8',
24
+ '.svg': 'image/svg+xml',
25
+ '.png': 'image/png',
26
+ '.jpg': 'image/jpeg',
27
+ '.jpeg': 'image/jpeg',
28
+ '.gif': 'image/gif',
29
+ '.webp': 'image/webp',
30
+ '.avif': 'image/avif',
31
+ '.ico': 'image/x-icon',
32
+ '.woff': 'font/woff',
33
+ '.woff2': 'font/woff2',
34
+ '.ttf': 'font/ttf',
35
+ '.otf': 'font/otf',
36
+ '.eot': 'application/vnd.ms-fontobject',
37
+ '.mp4': 'video/mp4',
38
+ '.webm': 'video/webm',
39
+ '.mp3': 'audio/mpeg',
40
+ '.wav': 'audio/wav',
41
+ '.ogg': 'audio/ogg',
42
+ '.pdf': 'application/pdf',
22
43
  };
23
44
 
45
+ let port;
24
46
  try {
25
- liveServer.start(params);
47
+ port = resolvePort(); // --port <n> or PORT env, defaults to 9090
26
48
  } catch (err) {
49
+ print.boldError(err.message);
50
+ process.exit(1);
51
+ }
52
+
53
+ // Live reload only when started by dev-serve.js (IPC channel present): watcher.js
54
+ // reports each successful rebuild, relayed here as { type: 'reload' }.
55
+ const liveReload = Boolean(process.send);
56
+ const reloadClients = new Set();
57
+
58
+ function isFile(filePath) {
59
+ try {
60
+ return fs.statSync(filePath).isFile();
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ // Exact file, then a directory's index.html (prerendered SEO pages), else nothing.
67
+ function resolveFile(pathname) {
68
+ let decoded;
69
+ try {
70
+ decoded = decodeURIComponent(pathname);
71
+ } catch {
72
+ return null;
73
+ }
74
+ const candidate = path.join(ROOT, decoded);
75
+ if (candidate !== ROOT && !candidate.startsWith(ROOT + path.sep)) return null;
76
+ if (isFile(candidate)) return candidate;
77
+ const index = path.join(candidate, 'index.html');
78
+ return isFile(index) ? index : null;
79
+ }
80
+
81
+ // Browsers send Accept: text/html for page navigations — those get the SPA shell when no
82
+ // file matches (whatever the URL looks like, dots included); a missing asset gets a 404.
83
+ function wantsHtml(req) {
84
+ return (req.headers.accept ?? '').includes('text/html');
85
+ }
86
+
87
+ function send(res, status, body, headers = {}) {
88
+ res.writeHead(status, { 'Cache-Control': 'no-store', ...headers });
89
+ res.end(body);
90
+ }
91
+
92
+ function serveFile(req, res, filePath) {
93
+ const type = MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
94
+ let body = fs.readFileSync(filePath);
95
+
96
+ if (liveReload && type.startsWith('text/html')) {
97
+ const html = body.toString('utf-8');
98
+ body = html.includes('</body>') ? html.replace('</body>', () => `${RELOAD_TAG}</body>`) : html + RELOAD_TAG;
99
+ }
100
+
101
+ send(res, 200, req.method === 'HEAD' ? undefined : body, { 'Content-Type': type });
102
+ }
103
+
104
+ function openReloadStream(req, res) {
105
+ res.writeHead(200, {
106
+ 'Content-Type': 'text/event-stream',
107
+ 'Cache-Control': 'no-store',
108
+ Connection: 'keep-alive',
109
+ });
110
+ res.write(': connected\n\n');
111
+ reloadClients.add(res);
112
+ req.on('close', () => reloadClients.delete(res));
113
+ }
114
+
115
+ const server = http.createServer((req, res) => {
116
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
117
+ return send(res, 405, 'Method Not Allowed', { Allow: 'GET, HEAD' });
118
+ }
119
+
120
+ const { pathname } = new URL(req.url, 'http://localhost');
121
+
122
+ if (liveReload && pathname === RELOAD_EVENTS) return openReloadStream(req, res);
123
+ if (liveReload && pathname === RELOAD_SCRIPT) {
124
+ return send(res, 200, RELOAD_CLIENT, { 'Content-Type': MIME_TYPES['.js'] });
125
+ }
126
+
127
+ const filePath = resolveFile(pathname) ?? (wantsHtml(req) ? resolveFile('/index.html') : null);
128
+ if (!filePath) return send(res, 404, 'Not Found', { 'Content-Type': MIME_TYPES['.txt'] });
129
+
130
+ serveFile(req, res, filePath);
131
+ });
132
+
133
+ server.on('error', (err) => {
27
134
  if (err.code === 'EADDRINUSE') {
28
- print.boldError(`Port ${params.port} is already in use — stop the process using it or change the dev port.`);
135
+ print.boldError(`Port ${port} is already in use. Pass another one with --port <n> (or PORT=<n>).`);
29
136
  } else {
30
- print.boldError(`live-server failed to start: ${err.message}`);
137
+ print.boldError(`Dev server failed to start: ${err.message}`);
31
138
  }
32
139
  process.exit(1);
140
+ });
141
+
142
+ // Loopback only, like the URL printed below: the dev server isn't exposed to the network.
143
+ server.listen(port, '127.0.0.1', () => {
144
+ const url = `http://127.0.0.1:${port}`;
145
+ print.boldInfo(`Serving at ${url}`);
146
+ // Started by dev-serve.js: report the URL so watcher.js can reprint it after rebuilds.
147
+ if (process.send) process.send({ type: 'server-url', url });
148
+ });
149
+
150
+ if (liveReload) {
151
+ process.on('message', (msg) => {
152
+ if (msg?.type !== 'reload') return;
153
+ for (const client of reloadClients) client.write('event: reload\ndata: {}\n\n');
154
+ });
155
+
156
+ // Keeps idle event streams from being closed by proxies/browsers.
157
+ setInterval(() => {
158
+ for (const client of reloadClients) client.write(': ping\n\n');
159
+ }, 30_000).unref();
160
+
161
+ // Started by dev-serve.js: exit with it even if it dies without cleaning up (kill -9, crash).
162
+ process.on('disconnect', () => process.exit(0));
33
163
  }
@@ -2,16 +2,43 @@ import chokidar from 'chokidar';
2
2
  import { exec } from 'child_process';
3
3
  import path from 'path';
4
4
  import { print } from '../utils/index.js'
5
+ {{#if markdown}}
6
+ import nutinConfig from '../../nutin.config.js';
7
+ {{/if}}
5
8
 
6
- const watcher = chokidar.watch(['src'], {
9
+ // config/, nutin.config.js and public/ are copied into the build too.
10
+ const watcher = chokidar.watch(['src', 'config', 'public', 'nutin.config.js'], {
7
11
  ignored: /(^|[/\\])\../,
8
12
  persistent: true,
13
+ ignoreInitial: true,
9
14
  });
15
+ {{#if markdown}}
16
+
17
+ // Markdown folders live outside src/ but still feed the build.
18
+ watcher.add(
19
+ (nutinConfig.markdownSources?.sourceFolders ?? [])
20
+ .map((entry) => (typeof entry === 'string' ? entry : entry?.folder))
21
+ .filter(Boolean)
22
+ );
23
+ {{/if}}
10
24
 
11
25
  let isBuilding = false;
12
26
  let pendingRebuild = false;
13
27
  let buildTimeout = null;
14
28
  let lastChangedPath = null;
29
+ let serverUrl = null;
30
+
31
+ function printWatching() {
32
+ if (serverUrl) print.boldInfo(`Serving at ${serverUrl}`);
33
+ print.boldBlue('Watching for changes...');
34
+ }
35
+
36
+ // Sent by dev-serve.js (relayed from serve.js) once the server is listening.
37
+ process.on('message', (msg) => {
38
+ if (msg?.type === 'server-url') serverUrl = msg.url;
39
+ });
40
+ // Exit with dev-serve.js even if it dies without cleaning up (kill -9, crash).
41
+ if (process.send) process.on('disconnect', () => process.exit(0));
15
42
 
16
43
  function runBuild() {
17
44
  isBuilding = true;
@@ -33,7 +60,9 @@ function runBuild() {
33
60
  print.boldError(`\nBuild failed: ${err.message}`);
34
61
  }
35
62
  } else {
36
- print.boldBlue('Watching for changes...');
63
+ printWatching();
64
+ // Relayed by dev-serve.js to serve.js, which tells open pages to reload.
65
+ if (process.send && process.connected) process.send({ type: 'reload' });
37
66
  }
38
67
  isBuilding = false;
39
68
 
@@ -48,7 +77,9 @@ watcher.on('error', (err) => {
48
77
  print.boldError(`\nWatcher error: ${err.message}`);
49
78
  });
50
79
 
51
- watcher.on('change', (filePath) => {
80
+ // New files (e.g. from `{{packageManager}} run generate`) and deletions need a rebuild too.
81
+ watcher.on('all', (event, filePath) => {
82
+ if (!['add', 'change', 'unlink'].includes(event)) return;
52
83
  lastChangedPath = filePath;
53
84
 
54
85
  if (isBuilding) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import path from "path";
4
- import { allFormats, getLastWord, print, promptBoolean, errorExit } from "../utils/index.js";
4
+ import { allFormats, getLastWord, print, errorExit } from "../utils/index.js";
5
5
  import fs from "fs";
6
6
  import { generateFile, generateLocalesJson } from "./handle-file.js";
7
7
  import { serviceTemplate, componentTemplate, viewTemplate, htmlTemplate, scssTemplate, testTemplate } from "./templates/index.js";
@@ -1,5 +1,7 @@
1
1
  import fs from 'fs';
2
- import { print, LANGUAGES } from '../utils/index.js';
2
+ import { print } from '../utils/index.js';
3
+ // Imported directly, not via the utils barrel: it reads config/languages.json on load.
4
+ import { LANGUAGES } from '../utils/languages.js';
3
5
  import { localeTemplate } from './templates/index.js';
4
6
 
5
7
  export function generateFile({
@@ -7,7 +9,7 @@ export function generateFile({
7
9
  targetPath,
8
10
  templateFn,
9
11
  suffix,
10
- extension = 'ts',
12
+ extension = '{{#if (eq lang "js")}}js{{else}}ts{{/if}}',
11
13
  }) {
12
14
  fs.mkdirSync(targetPath, { recursive: true });
13
15
  const template = templateFn(name, targetPath, suffix);
@@ -23,7 +23,7 @@ describe('handle-file', () => {
23
23
 
24
24
  generateFile({ name, targetPath, templateFn: (n) => `content for ${n.kebab}`, suffix: 'component' });
25
25
 
26
- const filePath = path.join(targetPath, 'widget.component.ts');
26
+ const filePath = path.join(targetPath, 'widget.component.{{#if (eq lang "js")}}js{{else}}ts{{/if}}');
27
27
  expect(fs.existsSync(filePath)).toBeTruthy();
28
28
  expect(fs.readFileSync(filePath, 'utf8')).toBe('content for widget');
29
29
  expect(exitSpy.callCount).toBe(0);
@@ -33,9 +33,9 @@ describe('handle-file', () => {
33
33
  const targetPath = path.join(tmpDir, 'services', 'widget');
34
34
  const name = { kebab: 'widget', pascal: 'Widget', camel: 'widget', capitalized: 'Widget' };
35
35
 
36
- generateFile({ name, targetPath, templateFn: () => 'x', suffix: 'service', extension: 'js' });
36
+ generateFile({ name, targetPath, templateFn: () => 'x', suffix: 'service', extension: 'scss' });
37
37
 
38
- expect(fs.existsSync(path.join(targetPath, 'widget.service.js'))).toBeTruthy();
38
+ expect(fs.existsSync(path.join(targetPath, 'widget.service.scss'))).toBeTruthy();
39
39
  });
40
40
 
41
41
  it('generateFile calls process.exit(1) instead of overwriting an existing file', () => {
@@ -4,12 +4,12 @@ export const componentTemplate = (name, targetPath) => {
4
4
  const relToCore = getRelToCore(targetPath);
5
5
  const htmlTemplate = '__TEMPLATE_PLACEHOLDER__';
6
6
 
7
- return `import { Component } from '${relToCore}';
7
+ return `import { Component, html } from '${relToCore}';
8
8
 
9
- const templateFn = () => \`${htmlTemplate}\`;
9
+ const templateFn = () => html\`${htmlTemplate}\`;
10
10
 
11
11
  export class ${name.pascal}Component extends Component {
12
- constructor(mountTarget: HTMLElement) {
12
+ constructor({{#if (eq lang "js")}}mountTarget{{else}}mountTarget: HTMLElement{{/if}}) {
13
13
  super({templateFn, mountTarget});
14
14
  }
15
15
  }
@@ -8,12 +8,12 @@ describe('componentTemplate', () => {
8
8
  // The generator always leaves the html body as a placeholder — the builder
9
9
  // swaps in the .html sibling's content at build time.
10
10
  expect(componentTemplate(name, targetPath)).toBe(
11
- `import { Component } from '../../../core/index.js';
11
+ `import { Component, html } from '../../../core/index.js';
12
12
 
13
- const templateFn = () => \`__TEMPLATE_PLACEHOLDER__\`;
13
+ const templateFn = () => html\`__TEMPLATE_PLACEHOLDER__\`;
14
14
 
15
15
  export class WidgetComponent extends Component {
16
- constructor(mountTarget: HTMLElement) {
16
+ constructor({{#if (eq lang "js")}}mountTarget{{else}}mountTarget: HTMLElement{{/if}}) {
17
17
  super({templateFn, mountTarget});
18
18
  }
19
19
  }
@@ -5,7 +5,7 @@ export const serviceTemplate = (name, targetPath) => {
5
5
 
6
6
  return `import { Service } from '${relToCore}';
7
7
 
8
- export class ${name.pascal}Service extends Service<${name.pascal}Service> {
8
+ export class ${name.pascal}Service extends Service{{#unless (eq lang "js")}}<${name.pascal}Service>{{/unless}} {
9
9
  constructor() {
10
10
  super();
11
11
  }
@@ -8,7 +8,7 @@ describe('serviceTemplate', () => {
8
8
  expect(serviceTemplate(name, targetPath)).toBe(
9
9
  `import { Service } from '../../../core/index.js';
10
10
 
11
- export class WidgetService extends Service<WidgetService> {
11
+ export class WidgetService extends Service{{#unless (eq lang "js")}}<WidgetService>{{/unless}} {
12
12
  constructor() {
13
13
  super();
14
14
  }
@@ -4,9 +4,9 @@ export const viewTemplate = (name, targetPath) => {
4
4
  const relToCore = getRelToCore(targetPath);
5
5
  const htmlTemplate = '__TEMPLATE_PLACEHOLDER__';
6
6
 
7
- return `import { View } from '${relToCore}';
7
+ return `import { View, html } from '${relToCore}';
8
8
 
9
- const template = \`${htmlTemplate}\`;
9
+ const template = html\`${htmlTemplate}\`;
10
10
 
11
11
  export class ${name.pascal}View extends View {
12
12
  constructor() {
@@ -8,9 +8,9 @@ describe('viewTemplate', () => {
8
8
  // The generator always leaves the html body as a placeholder — the builder
9
9
  // swaps in the .html sibling's content at build time.
10
10
  expect(viewTemplate(name, targetPath)).toBe(
11
- `import { View } from '../../../core/index.js';
11
+ `import { View, html } from '../../../core/index.js';
12
12
 
13
- const template = \`__TEMPLATE_PLACEHOLDER__\`;
13
+ const template = html\`__TEMPLATE_PLACEHOLDER__\`;
14
14
 
15
15
  export class WidgetView extends View {
16
16
  constructor() {
@@ -13,7 +13,7 @@ export async function maybeWriteUncoveredReport(report) {
13
13
  return;
14
14
  }
15
15
 
16
- if (!config.testinNutin.coverage.reportUncovered) return;
16
+ if (!config.testinNutin.coverage?.reportUncovered) return;
17
17
 
18
18
  try {
19
19
  fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
@@ -101,12 +101,12 @@ global.expect = (actual) => {
101
101
  }
102
102
  },
103
103
  toBeLessThan(expected) {
104
- if (actual > expected) {
104
+ if (!(actual < expected)) {
105
105
  throw new AssertionError(`Expected ${actual} to be less than ${expected}`)
106
106
  }
107
107
  },
108
108
  toBeGreaterThan(expected) {
109
- if (actual < expected) {
109
+ if (!(actual > expected)) {
110
110
  throw new AssertionError(`Expected ${actual} to be greater than ${expected}`)
111
111
  }
112
112
  },
@@ -94,4 +94,10 @@ describe('assertions', () => {
94
94
  }
95
95
  expect(caught).toBeInstanceOf(TypeError);
96
96
  });
97
+ it('toBeLessThan / toBeGreaterThan are strict comparisons', () => {
98
+ expect(1).toBeLessThan(2);
99
+ expect(2).toBeGreaterThan(1);
100
+ expect(2).not.toBeLessThan(2);
101
+ expect(2).not.toBeGreaterThan(2);
102
+ });
97
103
  });
@@ -23,15 +23,14 @@ export function registerTestGlobals() {
23
23
  fn();
24
24
  };
25
25
 
26
+ // The suite's hooks object is shared by reference (read when the queue runs), so a hook
27
+ // registered after some it() calls in the same describe() still applies to them.
26
28
  global.it = (name, fn) => {
27
- addTest({
28
- suiteName: currentSuite,
29
- testName: name,
29
+ addTest({
30
+ suiteName: currentSuite,
31
+ testName: name,
30
32
  testFn: fn,
31
- beforeEach: currentSuiteHooks.beforeEach,
32
- beforeAll: currentSuiteHooks.beforeAll,
33
- afterEach: currentSuiteHooks.afterEach,
34
- afterAll: currentSuiteHooks.afterAll
33
+ hooks: currentSuiteHooks,
35
34
  });
36
35
  };
37
36
  global.it.todo = (s) => {
@@ -26,3 +26,15 @@ describe('globals beforeEach/All and afterEach/All', () => {
26
26
  expect(afterEachCount).toEqual(1);
27
27
  });
28
28
  })
29
+
30
+ describe('Hooks registered after it()', () => {
31
+ let beforeEachCount = 0;
32
+
33
+ it('still get the beforeEach declared further down', () => {
34
+ expect(beforeEachCount).toBe(1);
35
+ });
36
+
37
+ beforeEach(() => {
38
+ beforeEachCount++;
39
+ });
40
+ });
@@ -39,8 +39,9 @@ export function printStart() {
39
39
 
40
40
  export function printCoverageReport(report) {
41
41
  if (!report.length) {
42
- print.warn('No app source files were covered.');
43
- return { globalLinesPct: 100, globalFunctionsPct: 100, globalBranchesPct: 100 };
42
+ // 0, not 100: an empty report must not pass a coverage threshold by default.
43
+ print.warn('No source files were covered.');
44
+ return { globalLinesPct: 0, globalFunctionsPct: 0, globalBranchesPct: 0 };
44
45
  }
45
46
 
46
47
  print.boldBlue('\nCoverage Summary');
@@ -77,9 +78,14 @@ export function printCoverageReport(report) {
77
78
 
78
79
  export function toTsPath(input) {
79
80
  return input
80
- .split('/')
81
+ .split(/[\\/]/)
81
82
  .slice(1)
83
+ {{#if (eq lang "js")}}
84
+ .join('/');
85
+ {{else}}
82
86
  .join('/')
83
- .replace(/\.js$/, '.ts');
87
+ // Test files stay .test.js in TypeScript projects too.
88
+ .replace(/(?<!\.test)\.js$/, '.ts');
89
+ {{/if}}
84
90
  }
85
91