@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,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,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from 'child_process';
4
- import { print, runCommand } from '../utils/index.js';
3
+ import { spawn, spawnSync } from 'child_process';
4
+ import { print, resolvePort, runCommand } from '../utils/index.js';
5
+
6
+ const isWindows = process.platform === 'win32';
5
7
 
6
8
  async function startDev() {
7
9
  console.clear();
8
10
  print.blue('🚀 Starting Dev Environment...\n');
9
11
 
12
+ let port;
13
+ try {
14
+ port = resolvePort();
15
+ } catch (err) {
16
+ print.boldError(err.message);
17
+ process.exit(1);
18
+ }
19
+
10
20
  try {
11
21
  await runCommand('{{packageManager}}', ['run', 'build', '--silent']);
12
22
  } catch (err) {
@@ -14,22 +24,76 @@ async function startDev() {
14
24
  process.exit(1);
15
25
  }
16
26
 
17
- const serve = spawn(['{{packageManager}}', 'run', 'serve:only', '--silent'].join(' '), { stdio: 'inherit', shell: true });
18
- const watcher = spawn(['node', 'tools/dev/watcher.js', '--silent'].join(' '), { stdio: 'inherit', shell: true });
27
+ // detached: true makes each child the leader of its own process group, so its
28
+ // descendants (watcher.js's own `exec('{{packageManager}} run build')` included) can be
29
+ // killed as a group below. Without this, a shutdown that doesn't cleanly propagate
30
+ // SIGINT down the shell layers (backgrounded, launched by a task runner, a closed
31
+ // terminal tab instead of Ctrl-C, ...) leaves them running as orphans — multiple
32
+ // stray watcher.js processes then race each other's builds.
33
+ // Not on Windows: there, detached children leave the console, so Ctrl-C would no
34
+ // longer reach them, and negative-pid group kills aren't supported — taskkill /T
35
+ // walks the process tree instead.
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
+ });
53
+
54
+ const children = [serve, watcher];
55
+ let shuttingDown = false;
56
+
57
+ function killGroup(child, signal) {
58
+ if (!child.pid) return;
59
+ if (isWindows) {
60
+ spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
61
+ return;
62
+ }
63
+ try {
64
+ process.kill(-child.pid, signal);
65
+ } catch {
66
+ // group already gone
67
+ }
68
+ }
69
+
70
+ function shutdown(signal, exitCode = 0) {
71
+ if (shuttingDown) return;
72
+ shuttingDown = true;
73
+ children.forEach((child) => killGroup(child, signal));
74
+ process.exit(exitCode);
75
+ }
76
+
77
+ process.on('SIGINT', () => shutdown('SIGINT'));
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'));
19
81
 
20
82
  serve.on('error', (err) => {
21
- print.boldError(`live-server failed to start: ${err.message}`);
83
+ print.boldError(`dev server failed to start: ${err.message}`);
22
84
  process.exit(1);
23
85
  });
24
86
  serve.on('close', (code) => {
25
- print.error(`live-server exited with code ${code}`);
26
- process.exit(code ?? 1);
87
+ if (shuttingDown) return;
88
+ print.error(`dev server exited with code ${code}`);
89
+ shutdown('SIGTERM', code ?? 1);
27
90
  });
28
91
  watcher.on('error', (err) => {
29
92
  print.boldError(`watcher failed to start: ${err.message}`);
30
93
  process.exit(1);
31
94
  });
32
95
  watcher.on('close', (code) => {
96
+ if (shuttingDown) return;
33
97
  print.error(`watcher exited with code ${code}`);
34
98
  });
35
99
  }
@@ -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,49 +2,96 @@ 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;
26
+ let pendingRebuild = false;
12
27
  let buildTimeout = null;
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));
42
+
43
+ function runBuild() {
44
+ isBuilding = true;
45
+ pendingRebuild = false;
46
+
47
+ console.clear();
48
+ print.boldInfo(`\n🔄 File changed: ${path.relative(process.cwd(), lastChangedPath)}\n`);
49
+ print.info('\nRebuilding...');
50
+
51
+ const command = '{{packageManager}} run build --silent';
52
+
53
+ exec(command, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
54
+ if (stdout) process.stdout.write(stdout);
55
+ if (stderr) process.stderr.write(stderr);
56
+ if (err) {
57
+ if (err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
58
+ print.boldError(`\nBuild output exceeded the buffer size — raise maxBuffer in tools/dev/watcher.js.`);
59
+ } else {
60
+ print.boldError(`\nBuild failed: ${err.message}`);
61
+ }
62
+ } else {
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' });
66
+ }
67
+ isBuilding = false;
68
+
69
+ // A change arrived while this build was running — it was never picked up by
70
+ // any debounce timer (isBuilding used to make those just silently return,
71
+ // dropping the change for good), so build again now instead of missing it.
72
+ if (pendingRebuild) runBuild();
73
+ });
74
+ }
13
75
 
14
76
  watcher.on('error', (err) => {
15
77
  print.boldError(`\nWatcher error: ${err.message}`);
16
78
  });
17
79
 
18
- 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;
83
+ lastChangedPath = filePath;
84
+
85
+ if (isBuilding) {
86
+ pendingRebuild = true;
87
+ return;
88
+ }
89
+
19
90
  if (buildTimeout) {
20
91
  clearTimeout(buildTimeout);
21
92
  }
22
-
23
- buildTimeout = setTimeout(() => {
24
- if (isBuilding) return;
25
- isBuilding = true;
26
-
27
- console.clear();
28
- print.boldInfo(`\n🔄 File changed: ${path.relative(process.cwd(), filePath)}\n`);
29
- print.info('\nRebuilding...');
30
-
31
- const command = '{{packageManager}} run build --silent';
32
-
33
- exec(command, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
34
- if (stdout) process.stdout.write(stdout);
35
- if (stderr) process.stderr.write(stderr);
36
- if (err) {
37
- if (err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
38
- print.boldError(`\nBuild output exceeded the buffer size — raise maxBuffer in tools/dev/watcher.js.`);
39
- } else {
40
- print.boldError(`\nBuild failed: ${err.message}`);
41
- }
42
- } else {
43
- print.boldBlue('Watching for changes...');
44
- }
45
- isBuilding = false;
46
- });
47
- }, 100);
93
+
94
+ buildTimeout = setTimeout(runBuild, 100);
48
95
  });
49
96
 
50
97
  watcher.on('ready', () => {
@@ -1,24 +1,12 @@
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";
5
- import { generateFile, appendToIndex, generateLocalesJson } from "./handle-file.js";
4
+ import { allFormats, getLastWord, print, errorExit } from "../utils/index.js";
5
+ import fs from "fs";
6
+ import { generateFile, generateLocalesJson } from "./handle-file.js";
6
7
  import { serviceTemplate, componentTemplate, viewTemplate, htmlTemplate, scssTemplate, testTemplate } from "./templates/index.js";
7
8
  import nutinConfig from "../../nutin.config.js";
8
9
 
9
- // Constants and Setup
10
- const [, , rawType, rawFullPath] = process.argv;
11
-
12
- if (!rawType || !rawFullPath) {
13
- showUsageAndExit("Missing arguments.");
14
- process.exit(1);
15
- }
16
-
17
- const type = allFormats(rawType);
18
- const fullPath = allFormats(rawFullPath);
19
- const name = allFormats(getLastWord(fullPath.kebab));
20
- const targetPath = path.join('src', 'app', `${type.kebab}s/${fullPath.kebab}`);
21
-
22
10
  // Creator Mapping
23
11
  const creators = {
24
12
  service: async (name, targetPath) => {
@@ -26,7 +14,6 @@ const creators = {
26
14
  print.section(`Generating service: ${name.capitalized}`);
27
15
  try {
28
16
  generateFile({ name, targetPath, templateFn: serviceTemplate, suffix: suffix });
29
- appendToIndex({ name, targetPath, suffix: suffix });
30
17
  await generateTest({ name, targetPath, suffix: suffix });
31
18
  } catch (err) {
32
19
  handleError("Failed to generate service", err);
@@ -41,7 +28,6 @@ const creators = {
41
28
  generateFile({ name, targetPath, templateFn: htmlTemplate, suffix: suffix, extension: 'html' });
42
29
  generateFile({ name, targetPath, templateFn: scssTemplate, suffix: suffix, extension: 'scss' });
43
30
  await generateLocales({ targetPath, name, isView: false });
44
- appendToIndex({ name, targetPath, suffix: suffix });
45
31
  await generateTest({ name, targetPath, suffix });
46
32
  } catch (err) {
47
33
  handleError("Failed to generate component", err);
@@ -56,7 +42,6 @@ const creators = {
56
42
  generateFile({ name, targetPath, templateFn: htmlTemplate, suffix: suffix, extension: 'html' });
57
43
  generateFile({ name, targetPath, templateFn: scssTemplate, suffix: suffix, extension: 'scss' });
58
44
  await generateLocales({ targetPath, name, isView: true });
59
- appendToIndex({ name, targetPath, suffix: suffix });
60
45
  await generateTest({ name, targetPath, suffix });
61
46
  } catch (err) {
62
47
  handleError("Failed to generate view", err);
@@ -64,10 +49,30 @@ const creators = {
64
49
  },
65
50
  };
66
51
 
52
+ // Constants and Setup
53
+ const [, , rawType, rawFullPath] = process.argv;
54
+
55
+ if (!rawType || !rawFullPath) {
56
+ showUsageAndExit("Missing arguments.");
57
+ process.exit(1);
58
+ }
59
+
60
+ const type = allFormats(rawType);
61
+ const fullPath = allFormats(rawFullPath);
62
+ const name = allFormats(getLastWord(fullPath.kebab));
63
+ const targetPath = path.join('src', 'app', fullPath.kebab);
64
+
67
65
  // Main Execution
68
66
  const create = creators[type.kebab];
69
67
 
70
68
  if (create) {
69
+ // Folder names carry no type suffix, so a view and a component with the same path would
70
+ // share one folder (and clobber each other's locales/) — refuse before writing anything.
71
+ if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0) {
72
+ print.boldError(`\nA folder with this name already exists: ${targetPath}`);
73
+ print.warn("Pick another path, e.g. user/user-view and user/user-card.");
74
+ process.exit(1);
75
+ }
71
76
  await create(name, targetPath);
72
77
  print.boldSuccess(`\n${type.capitalized} ${name.capitalized} generated in ${targetPath}.\n`)
73
78
  } else {
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
- import path from 'path';
3
- 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';
4
5
  import { localeTemplate } from './templates/index.js';
5
6
 
6
7
  export function generateFile({
@@ -8,7 +9,7 @@ export function generateFile({
8
9
  targetPath,
9
10
  templateFn,
10
11
  suffix,
11
- extension = 'ts',
12
+ extension = '{{#if (eq lang "js")}}js{{else}}ts{{/if}}',
12
13
  }) {
13
14
  fs.mkdirSync(targetPath, { recursive: true });
14
15
  const template = templateFn(name, targetPath, suffix);
@@ -22,27 +23,6 @@ export function generateFile({
22
23
  fs.writeFileSync(filePath, template);
23
24
  }
24
25
 
25
- export function appendToIndex({ name, targetPath, suffix }) {
26
- const absTargetPath = path.resolve(process.cwd(), targetPath);
27
-
28
- const parts = targetPath.split(path.sep);
29
- const basePath = parts.slice(0, 3).join(path.sep);
30
-
31
- const indexFilePath = path.join(basePath, 'index.ts');
32
-
33
- const absFilePath = path.join(absTargetPath, `${name.kebab}.${suffix}.js`);
34
- const relPath = path.relative(basePath, absFilePath).replace(/\\/g, '/');
35
-
36
- const lineToAppend = `export * from './${relPath}';\n`;
37
-
38
- try {
39
- fs.appendFileSync(indexFilePath, lineToAppend, 'utf8');
40
- print.gray(`${suffix}s/index.ts updated.`);
41
- } catch (err) {
42
- throw new Error(`Failed to update ${indexFilePath}: ${err.message}`, { cause: err });
43
- }
44
- }
45
-
46
26
  export function generateLocalesJson({ targetPath, name, isView }) {
47
27
  const localesDir = `${targetPath}/locales`;
48
28
  fs.mkdirSync(localesDir, { recursive: true });