@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
@@ -16,6 +16,7 @@ export class FeatureContextBuilder {
16
16
  return {
17
17
  projectName: packageJson.name,
18
18
  packageManager,
19
+ lang: meta.lang ?? 'ts',
19
20
  ciCommand: getCiCommand(packageManager),
20
21
  version: PACKAGE_VERSION,
21
22
  ...(meta.features ?? {}),
@@ -5,25 +5,51 @@ import { print } from '../utils/print.mjs';
5
5
  const fs = fsExtra.default;
6
6
 
7
7
  const GENERATE_SEO_LINE_RE = /^([ \t]*generateSEOFiles:[^\n]*\n)/m;
8
- const DOCKER_PORTS_LINE = '\n// Nutin features\n dockerPorts: [9090], // Ports the Docker container exposes/listens on — edit as needed\n';
8
+ const FEATURES_HEADER_RE = /^([ \t]*\/\/ Nutin features[^\n]*\n)/m;
9
+ const FEATURES_HEADER = '\n // Nutin features\n';
10
+
11
+ const FEATURE_CONFIGS = {
12
+ docker: {
13
+ key: 'dockerPorts',
14
+ lines: ' dockerPorts: [9090], // Ports the Docker container exposes/listens on — edit as needed\n',
15
+ added: 'Added "dockerPorts" to nutin.config.js — edit it if you need different port(s).',
16
+ manual: 'add "dockerPorts: [9090]" to it manually before building.',
17
+ },
18
+ markdown: {
19
+ key: 'markdownSources',
20
+ lines:
21
+ ' markdownSources: { // Markdown folders compiled to /generated/<name>.json at build time\n' +
22
+ " sourceFolders: ['markdown-content'], // folder paths, or { folder, hubFiles, routePrefix, sectionInPath, prefixReplacements, seo }\n" +
23
+ ' },\n',
24
+ added: 'Added "markdownSources" to nutin.config.js — add your own folders to "sourceFolders".',
25
+ manual: 'add "markdownSources: { sourceFolders: [\'markdown-content\'] }" to it manually.',
26
+ },
27
+ };
9
28
 
10
29
  export async function updateNutinConfig(projectPath, feature) {
11
- if (feature.key !== 'docker') return;
30
+ const featureConfig = FEATURE_CONFIGS[feature.key];
31
+ if (!featureConfig) return;
12
32
 
13
33
  const configPath = path.join(projectPath, 'nutin.config.js');
14
34
  const content = await fs.readFile(configPath, 'utf-8');
15
35
 
16
- if (/dockerPorts\s*:/.test(content)) {
17
- print.info('Kept existing "dockerPorts" value in nutin.config.js');
36
+ if (new RegExp(`${featureConfig.key}\\s*:`).test(content)) {
37
+ print.info(`Kept existing "${featureConfig.key}" value in nutin.config.js`);
18
38
  return;
19
39
  }
20
40
 
21
- if (!GENERATE_SEO_LINE_RE.test(content)) {
22
- print.warn('Could not find a "generateSEOFiles:" line in nutin.config.js — add "dockerPorts: [9090]" to it manually before building.');
41
+ // Features share one "// Nutin features" block, created after generateSEOFiles by
42
+ // whichever feature is added first.
43
+ let updated;
44
+ if (FEATURES_HEADER_RE.test(content)) {
45
+ updated = content.replace(FEATURES_HEADER_RE, `$1${featureConfig.lines}`);
46
+ } else if (GENERATE_SEO_LINE_RE.test(content)) {
47
+ updated = content.replace(GENERATE_SEO_LINE_RE, `$1${FEATURES_HEADER}${featureConfig.lines}`);
48
+ } else {
49
+ print.warn(`Could not find a "generateSEOFiles:" line in nutin.config.js — ${featureConfig.manual}`);
23
50
  return;
24
51
  }
25
52
 
26
- const updated = content.replace(GENERATE_SEO_LINE_RE, `$1${DOCKER_PORTS_LINE}`);
27
53
  await fs.writeFile(configPath, updated);
28
- print.warn('Added "dockerPorts" to nutin.config.js — edit it if you need different port(s).');
54
+ print.warn(featureConfig.added);
29
55
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import * as fsExtra from 'fs-extra';
3
3
  import { print } from '../utils/print.mjs';
4
- import { getDockerScripts } from '../common/package-json-helper.mjs';
4
+ import { getDockerScripts, getMarkdownScripts } from '../common/package-json-helper.mjs';
5
5
 
6
6
  const fs = fsExtra.default;
7
7
 
@@ -26,15 +26,13 @@ export async function updatePackageJson(projectPath, feature) {
26
26
  const packageJsonPath = path.join(projectPath, 'package.json');
27
27
  const packageJson = await fs.readJSON(packageJsonPath);
28
28
 
29
- const isDocker = feature.key === 'docker';
29
+ const featureScripts = {
30
+ docker: getDockerScripts,
31
+ markdown: getMarkdownScripts,
32
+ };
30
33
 
31
- let scripts = {};
32
-
33
- if (isDocker) {
34
- scripts = getDockerScripts();
35
- } else {
36
- return;
37
- }
34
+ if (!featureScripts[feature.key]) return;
35
+ const scripts = featureScripts[feature.key]();
38
36
 
39
37
  const scriptsResult = mergeAdditive(packageJson.scripts ?? {}, scripts);
40
38
  packageJson.scripts = scriptsResult.merged;
@@ -4,6 +4,10 @@ export const FEATURES = [
4
4
  key: 'docker',
5
5
  cli: 'docker',
6
6
  },
7
+ {
8
+ key: 'markdown',
9
+ cli: 'markdown',
10
+ },
7
11
  ];
8
12
 
9
13
  export function findFeatureByCli(cliName) {
@@ -4,6 +4,7 @@ import * as fsExtra from 'fs-extra';
4
4
  import { TemplateCompiler } from './template-compiler.mjs';
5
5
  import { print } from '../utils/print.mjs';
6
6
  import { FEATURES } from './feature-registry.mjs';
7
+ import { toJsOutput } from './transpile-ts.mjs';
7
8
 
8
9
  const fs = fsExtra.default;
9
10
  const __filename = fileURLToPath(import.meta.url);
@@ -16,6 +17,17 @@ const BINARY_EXTENSIONS = new Set([
16
17
  '.pdf', '.zip', '.tar', '.gz'
17
18
  ]);
18
19
 
20
+ // Template-repo housekeeping, never generated. .gitkeep only keeps otherwise-empty
21
+ // template folders in git/npm; the folder itself is still created (ensureDir).
22
+ const IGNORED_TEMPLATE_FILES = new Set(['.DS_Store', '.gitkeep']);
23
+
24
+ // Templates that only make sense in a TypeScript project — not generated for JS-only ones.
25
+ const TS_ONLY_TEMPLATES = [
26
+ path.join('tools', 'builder', 'core', 'app', 'compile-ts.js.hbs'),
27
+ path.join('tools', 'builder', 'core', 'app', 'handler-refs.js.hbs'),
28
+ path.join('tools', 'builder', 'core', 'app', 'handler-refs.test.js.hbs'),
29
+ ];
30
+
19
31
  export class FileGenerator {
20
32
  constructor() {
21
33
  this.compiler = new TemplateCompiler();
@@ -42,7 +54,7 @@ export class FileGenerator {
42
54
  const entries = await fs.readdir(templateDir, { withFileTypes: true });
43
55
 
44
56
  for (const entry of entries) {
45
- if (entry.name === '.DS_Store') continue;
57
+ if (IGNORED_TEMPLATE_FILES.has(entry.name)) continue;
46
58
 
47
59
  const templatePath = path.join(templateDir, entry.name);
48
60
 
@@ -76,8 +88,13 @@ export class FileGenerator {
76
88
  }
77
89
 
78
90
  async isRenderTemplateFile(templatePath, fileName, context) {
91
+ const isJsOnly = context.lang === 'js';
92
+ if (isJsOnly && TS_ONLY_TEMPLATES.some((template) => templatePath.endsWith(template))) {
93
+ return null;
94
+ }
95
+
79
96
  const fileExt = path.extname(fileName).toLowerCase();
80
- const outputFileName = fileName.endsWith('.hbs') ? fileName.replace('.hbs', '') : fileName;
97
+ const outputFileName = fileName.replace(/\.hbs$/, '');
81
98
  const isBinary = BINARY_EXTENSIONS.has(fileExt);
82
99
 
83
100
  let content;
@@ -89,6 +106,11 @@ export class FileGenerator {
89
106
  content = await fs.readFile(templatePath, 'utf8');
90
107
  }
91
108
 
109
+ if (isJsOnly && !isBinary) {
110
+ const jsOutput = await toJsOutput(outputFileName, content);
111
+ return jsOutput && { ...jsOutput, isBinary };
112
+ }
113
+
92
114
  return { outputFileName, isBinary, content };
93
115
  }
94
116
 
@@ -140,7 +162,7 @@ export class FileGenerator {
140
162
  const entries = await fs.readdir(dir, { withFileTypes: true });
141
163
 
142
164
  for (const entry of entries) {
143
- if (entry.name === '.DS_Store') continue;
165
+ if (IGNORED_TEMPLATE_FILES.has(entry.name)) continue;
144
166
 
145
167
  const entryPath = path.join(dir, entry.name);
146
168
 
@@ -0,0 +1,31 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ export const execFileAsync = promisify(execFile);
5
+
6
+ // nutin new runs `git init` with it, and nutin-update needs it to merge files you edited.
7
+ export async function isGitAvailable() {
8
+ try {
9
+ await execFileAsync('git', ['--version']);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ // The project folder's uncommitted changes, untracked files included (ignored ones aren't listed).
17
+ // Scoped to the folder, so a project inside a bigger repository isn't blocked by changes elsewhere.
18
+ // `repo: false` when git is missing or the folder isn't in a repository.
19
+ export async function getWorkingTreeStatus(projectPath) {
20
+ try {
21
+ await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: projectPath });
22
+ } catch {
23
+ return { repo: false, gitFound: await isGitAvailable(), changes: [] };
24
+ }
25
+ const { stdout } = await execFileAsync(
26
+ 'git',
27
+ ['status', '--porcelain', '--untracked-files=all', '--', '.'],
28
+ { cwd: projectPath, maxBuffer: 1024 * 1024 * 10 },
29
+ );
30
+ return { repo: true, gitFound: true, changes: stdout.split('\n').filter(Boolean) };
31
+ }
@@ -1,11 +1,35 @@
1
+ import path from 'path';
2
+ import * as fsExtra from 'fs-extra';
1
3
  import inquirer from 'inquirer';
2
4
  import { print } from '../utils/print.mjs';
3
5
  import { FEATURES } from './feature-registry.mjs';
4
6
  import { detectPackageManager } from './package-json-helper.mjs';
5
7
  import { writeProjectMeta } from './project-meta.mjs';
8
+ import { META_FILE_NAME } from './package-data.mjs';
9
+
10
+ const fs = fsExtra.default;
11
+
12
+ // `--js-only` apps have src/app/main.js and no main.ts. Projects from before JS-only
13
+ // support are all TypeScript, so anything else counts as 'ts'.
14
+ async function detectLang(projectPath) {
15
+ const appDir = path.join(projectPath, 'src', 'app');
16
+ const hasMainJs = await fs.pathExists(path.join(appDir, 'main.js'));
17
+ const hasMainTs = await fs.pathExists(path.join(appDir, 'main.ts'));
18
+ return hasMainJs && !hasMainTs ? 'js' : 'ts';
19
+ }
6
20
 
7
21
  export async function bootstrapProjectMeta(projectPath) {
8
- print.warn('āš ļø No .nutin-meta.json found for this project.');
22
+ // inquirer can't prompt without a TTY: it throws on a closed stdin and waits forever on an open pipe.
23
+ if (!process.stdin.isTTY) {
24
+ throw new Error(
25
+ `No ${META_FILE_NAME} found for this project, and this shell is non-interactive, so it can't be reconstructed. ` +
26
+ `Run this command again in a terminal to answer a couple of questions, or create ${META_FILE_NAME} yourself, e.g. ` +
27
+ `{ "version": "1.3.1", "packageManager": "npm", "lang": "ts", "features": { "docker": false, "markdown": false } } ` +
28
+ `("lang": "js" for a --js-only project)`
29
+ );
30
+ }
31
+
32
+ print.warn(`āš ļø No ${META_FILE_NAME} found for this project.`);
9
33
  print.section('Answer a couple of questions so a baseline can be reconstructed:\n');
10
34
 
11
35
  const { version } = await inquirer.prompt([
@@ -27,7 +51,9 @@ export async function bootstrapProjectMeta(projectPath) {
27
51
  ]);
28
52
 
29
53
  const packageManager = await detectPackageManager(projectPath);
54
+ const lang = await detectLang(projectPath);
30
55
  const flags = Object.fromEntries(FEATURES.map((f) => [f.key, features.includes(f.key)]));
31
56
 
32
- return writeProjectMeta(projectPath, { version: version.trim(), packageManager, ...flags });
57
+ print.gray(`Detected ${lang === 'js' ? 'a JavaScript' : 'a TypeScript'} project using ${packageManager} — edit ${META_FILE_NAME} if that's wrong.`);
58
+ return writeProjectMeta(projectPath, { version: version.trim(), packageManager, lang, ...flags });
33
59
  }
@@ -5,6 +5,8 @@ import * as path from 'path';
5
5
 
6
6
  const fs = fsExtra.default;
7
7
 
8
+ export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
9
+
8
10
  export async function detectPackageManager(projectPath) {
9
11
  if (await fs.pathExists(path.join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm';
10
12
  if (await fs.pathExists(path.join(projectPath, 'yarn.lock'))) return 'yarn';
@@ -78,18 +80,25 @@ export function getDockerScripts() {
78
80
  };
79
81
  }
80
82
 
83
+ export function getMarkdownScripts() {
84
+ return {
85
+ "markdown:check": "node tools/builder/core/markdown/generate-markdown-pages.js --check"
86
+ };
87
+ }
88
+
81
89
  export function getAllScripts(context) {
82
- const { packageManager, docker } = context;
90
+ const { packageManager, docker, markdown } = context;
83
91
 
84
92
  const baseScripts = {
85
93
  "build": "node tools/builder/builder.js",
86
- "build:prod": "NODE_ENV=production node tools/builder/builder.js",
87
- "serve": `${packageManager} run build && ${packageManager} run serve:only`,
94
+ "build:prod": "node tools/builder/builder.js --prod",
95
+ "serve": `${packageManager} run build && node tools/dev/serve.js`,
88
96
  "serve:only": "node tools/dev/serve.js",
89
- "serve:prod": `${packageManager} run build:prod && ${packageManager} run serve:only`,
97
+ "serve:prod": `${packageManager} run build:prod && node tools/dev/serve.js`,
90
98
  "dev": "node tools/dev/dev-serve.js",
91
99
  "generate": "node tools/generator/generator.js",
92
100
  "testin-nutin": `${packageManager} run build && node tools/testin-nutin/runner.js`,
101
+ "testin-nutin:only": "node tools/testin-nutin/runner.js",
93
102
  "testin-nutin:watch": `${packageManager} run build && node tools/testin-nutin/watch-tests.js`,
94
103
  "testin-nutin:coverage": `${packageManager} run build && node tools/testin-nutin/runner.js --coverage`,
95
104
  "testin-nutin:verbose": `${packageManager} run build && node tools/testin-nutin/runner.js --verbose`
@@ -98,6 +107,7 @@ export function getAllScripts(context) {
98
107
  let scripts = { ...baseScripts };
99
108
 
100
109
  if (docker) scripts = { ...scripts, ...getDockerScripts() };
110
+ if (markdown) scripts = { ...scripts, ...getMarkdownScripts() };
101
111
 
102
112
  return scripts;
103
113
  }
@@ -1,11 +1,10 @@
1
1
  import path from 'path';
2
2
  import * as fsExtra from 'fs-extra';
3
3
  import { FEATURES } from './feature-registry.mjs';
4
+ import { META_FILE_NAME } from './package-data.mjs';
4
5
 
5
6
  const fs = fsExtra.default;
6
7
 
7
- const META_FILE_NAME = '.nutin-meta.json';
8
-
9
8
  function metaPath(projectPath) {
10
9
  return path.join(projectPath, META_FILE_NAME);
11
10
  }
@@ -22,6 +21,7 @@ export async function writeProjectMeta(projectPath, context) {
22
21
  const meta = {
23
22
  version: context.version,
24
23
  packageManager: context.packageManager,
24
+ lang: context.lang ?? 'ts',
25
25
  features: extractFeatures(context),
26
26
  };
27
27
 
@@ -34,6 +34,8 @@ export class TemplateCompiler {
34
34
  return a === b;
35
35
  });
36
36
 
37
+ // Unused by the current templates, but kept: nutin-update renders an older version's
38
+ // templates with this compiler, and those may still call it.
37
39
  Handlebars.registerHelper('hasAnyFeature', function(options) {
38
40
  const context = options.data.root;
39
41
  return context.i18n || context.template || context.testinNutin || context.stylinNutin
@@ -0,0 +1,94 @@
1
+ // JS-only projects: the templates stay TypeScript-authored, and each rendered .ts file is
2
+ // transpiled here at generation time. typescript is imported lazily so TS projects never load it.
3
+ // Its version is pinned in package.json: nutin-update re-transpiles the old baseline and compares
4
+ // it to the user's files, so any output change between versions would read as a user edit.
5
+ // This pin is the CLI's own: generated apps install their own TypeScript (7.x) for tsc.
6
+ let tsModule;
7
+
8
+ async function loadTypeScript() {
9
+ tsModule ??= (await import('typescript')).default;
10
+ return tsModule;
11
+ }
12
+
13
+ // TypeScript's printer drops blank lines and indents by 4. Blank lines survive as marker
14
+ // comments, which are turned back into blank lines after transpiling.
15
+ const BLANK_LINE_MARKER = '//__NUTIN_BLANK_LINE__';
16
+ const BLANK_LINE_MARKER_LINE = /^[ \t]*\/\/__NUTIN_BLANK_LINE__[ \t]*$/;
17
+
18
+ function markBlankLines(source) {
19
+ return source
20
+ .split('\n')
21
+ .map((line) => (line.trim() === '' ? BLANK_LINE_MARKER : line))
22
+ .join('\n');
23
+ }
24
+
25
+ function restoreBlankLines(output) {
26
+ return output
27
+ .split('\n')
28
+ .map((line) => (BLANK_LINE_MARKER_LINE.test(line) ? '' : line))
29
+ .join('\n')
30
+ .replace(/\n{3,}/g, '\n\n')
31
+ .replace(/^\n+/, '')
32
+ .replace(/\n*$/, '\n');
33
+ }
34
+
35
+ // Template literal bodies are emitted verbatim (already 2-space), so lines starting inside
36
+ // one are left alone.
37
+ function getTemplateLiteralRanges(ts, code) {
38
+ const sourceFile = ts.createSourceFile('output.js', code, ts.ScriptTarget.Latest, true);
39
+ const ranges = [];
40
+
41
+ (function visit(node) {
42
+ if (ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
43
+ ranges.push([node.getStart(sourceFile), node.end]);
44
+ return;
45
+ }
46
+ ts.forEachChild(node, visit);
47
+ })(sourceFile);
48
+
49
+ return ranges;
50
+ }
51
+
52
+ // Each 4-space level becomes 2; a smaller remainder (JSDoc ` * ` alignment) is kept.
53
+ function reindent(ts, code) {
54
+ const ranges = getTemplateLiteralRanges(ts, code);
55
+ let offset = 0;
56
+
57
+ return code
58
+ .split('\n')
59
+ .map((line) => {
60
+ const lineStart = offset;
61
+ offset += line.length + 1;
62
+
63
+ if (ranges.some(([start, end]) => lineStart > start && lineStart < end)) return line;
64
+
65
+ const indent = line.match(/^ */)[0].length;
66
+ return ' '.repeat(indent - 2 * Math.floor(indent / 4)) + line.slice(indent);
67
+ })
68
+ .join('\n');
69
+ }
70
+
71
+ /**
72
+ * Returns `{ outputFileName, content }` with a `.js` name and transpiled content,
73
+ * `null` for ambient `.d.ts` files (type-only, nothing to emit), or the input unchanged
74
+ * for any other file.
75
+ */
76
+ export async function toJsOutput(outputFileName, content) {
77
+ if (outputFileName.endsWith('.d.ts')) return null;
78
+ if (!outputFileName.endsWith('.ts')) return { outputFileName, content };
79
+
80
+ const ts = await loadTypeScript();
81
+ const { outputText } = ts.transpileModule(markBlankLines(content), {
82
+ fileName: outputFileName,
83
+ compilerOptions: {
84
+ target: ts.ScriptTarget.ESNext,
85
+ module: ts.ModuleKind.ESNext,
86
+ removeComments: false,
87
+ },
88
+ });
89
+
90
+ return {
91
+ outputFileName: outputFileName.replace(/\.ts$/, '.js'),
92
+ content: restoreBlankLines(reindent(ts, outputText)),
93
+ };
94
+ }
@@ -14,6 +14,7 @@ export class ContextBuilder {
14
14
  return {
15
15
  projectName: preferences.projectName,
16
16
  packageManager: preferences.packageManager,
17
+ lang: preferences.lang,
17
18
  ciCommand: ciCommand,
18
19
  version: version
19
20
  };
@@ -1,7 +1,17 @@
1
1
  import { print } from '../utils/print.mjs';
2
- import { promiseExec } from '../utils/promise-exec-alias.mjs';
2
+ import { execFileAsync, isGitAvailable } from '../common/git.mjs';
3
3
 
4
+ // A missing or failing git doesn't stop project creation: the project works without a repository.
4
5
  export async function initializeGit(projectPath) {
6
+ if (!(await isGitAvailable())) {
7
+ print.warn('āš ļø git was not found — skipping "git init". Install git to version the project; nutin-update also needs it to merge files you edit.');
8
+ return;
9
+ }
10
+
5
11
  print.section('āš™ļø Initializing Git repository...');
6
- await promiseExec('git init', { cwd: projectPath });
12
+ try {
13
+ await execFileAsync('git', ['init'], { cwd: projectPath });
14
+ } catch (error) {
15
+ print.warn(`āš ļø "git init" failed — skipping it (${(error.stderr || error.message).trim()}). Run it yourself once the issue is fixed.`);
16
+ }
7
17
  }
@@ -7,26 +7,34 @@ const fs = fsExtra.default;
7
7
  export class JsonManager {
8
8
  async generateJsonFiles(projectPath, context) {
9
9
  await this.generatePackageJson(projectPath, context);
10
- await this.generateTsconfigJson(projectPath, context);
10
+ if (context.lang !== 'js') await this.generateTsconfigJson(projectPath);
11
11
  }
12
12
 
13
13
  async generatePackageJson(projectPath, context) {
14
- const { testinNutin, projectName } = context;
14
+ await fs.writeJSON(path.join(projectPath, 'package.json'), this.buildPackageJson(context), { spaces: 2 });
15
+ }
16
+
17
+ async generateTsconfigJson(projectPath) {
18
+ await fs.writeJSON(path.join(projectPath, 'tsconfig.json'), this.buildTsconfigJson(), { spaces: 2 });
19
+ }
20
+
21
+ // Also used by nutin-update to compare an existing project's files with the current defaults.
22
+ buildPackageJson(context) {
23
+ const { projectName, lang } = context;
15
24
 
16
25
  const devDependencies = {
17
- "chokidar": "^4.0.3",
18
- "esbuild": "^0.25.12",
26
+ "chokidar": "^5.0.0",
27
+ "esbuild": "^0.28.2",
19
28
  "html-minifier-terser": "^7.2.0",
20
- "jsdom": "^26.1.0",
21
- "linkedom": "^0.18.12",
22
- "live-server": "^1.2.2",
29
+ "jsdom": "^30.1.1",
30
+ "linkedom": "^0.18.13",
23
31
  "sass": "^1.89.0",
24
- "typescript": "^5.8.3"
32
+ ...(lang === 'js' ? {} : { "typescript": "^7.0.2" })
25
33
  };
26
34
 
27
35
  const scripts = getAllScripts(context);
28
36
 
29
- const packageJson = {
37
+ return {
30
38
  "name": projectName,
31
39
  "version": "0.1.0",
32
40
  "type": "module",
@@ -36,18 +44,14 @@ export class JsonManager {
36
44
  scripts,
37
45
  devDependencies,
38
46
  "engines": {
39
- "node": ">=22"
47
+ "node": ">=24"
40
48
  }
41
49
  };
42
-
43
- await fs.writeJSON(path.join(projectPath, 'package.json'), packageJson, { spaces: 2 });
44
50
  }
45
51
 
46
- async generateTsconfigJson(projectPath) {
47
- const tsconfig = {
52
+ buildTsconfigJson() {
53
+ return {
48
54
  "compilerOptions": {
49
- "baseUrl": "./",
50
- "paths": {},
51
55
  "target": "ESNext",
52
56
  "module": "NodeNext",
53
57
  "moduleResolution": "NodeNext",
@@ -57,6 +61,7 @@ export class JsonManager {
57
61
  "skipLibCheck": true,
58
62
  "forceConsistentCasingInFileNames": true,
59
63
  "noUncheckedIndexedAccess": true,
64
+ "noUnusedLocals": true,
60
65
  "esModuleInterop": true,
61
66
  "allowSyntheticDefaultImports": true,
62
67
  "lib": ["es2022", "DOM"],
@@ -67,7 +72,5 @@ export class JsonManager {
67
72
  "include": ["src/app", "src/core"],
68
73
  "exclude": ["node_modules"]
69
74
  };
70
-
71
- await fs.writeJSON(path.join(projectPath, 'tsconfig.json'), tsconfig, { spaces: 2 });
72
75
  }
73
76
  }
@@ -1,5 +1,6 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { defaults } from './context-builder.mjs';
3
+ import { PACKAGE_MANAGERS } from '../common/package-json-helper.mjs';
3
4
 
4
5
  export async function newAppPrompt(initialName, cliOptions = {}) {
5
6
 
@@ -26,10 +27,10 @@ export async function newAppPrompt(initialName, cliOptions = {}) {
26
27
  if (!packageManager) {
27
28
  const pmPreference = await inquirer.prompt([
28
29
  {
29
- type: 'list',
30
+ type: 'select',
30
31
  name: 'packageManager',
31
32
  message: 'Package manager:',
32
- choices: ['npm', 'yarn', 'pnpm', 'bun'],
33
+ choices: PACKAGE_MANAGERS,
33
34
  default: defaults.packageManager,
34
35
  },
35
36
  ]);
@@ -39,6 +40,7 @@ export async function newAppPrompt(initialName, cliOptions = {}) {
39
40
  return {
40
41
  projectName,
41
42
  packageManager,
43
+ lang: cliOptions.jsOnly ? 'js' : 'ts',
42
44
  };
43
45
  }
44
46
 
@@ -2,7 +2,7 @@ import { print } from '../utils/print.mjs';
2
2
  import { PACKAGE_HOMEPAGE } from '../common/package-data.mjs';
3
3
 
4
4
  export function displaySuccessMessage(answers) {
5
- const { projectName, packageManager, deployHelper } = answers;
5
+ const { projectName, packageManager } = answers;
6
6
  print.info(`Visit https://${PACKAGE_HOMEPAGE}`);
7
7
 
8
8
  print.boldSuccess('\nšŸŽ‰ Your project is ready!\n');
@@ -1,31 +1,43 @@
1
1
  import path from 'path';
2
2
  import * as fsExtra from 'fs-extra';
3
- import * as Diff from 'diff';
3
+ import { CONFLICT_MARKER } from './three-way-merge.mjs';
4
4
 
5
5
  const fs = fsExtra.default;
6
- const REPORT_FILE_NAME = 'NUTIN-UPDATE-REPORT.md';
6
+ export const REPORT_FILE_NAME = 'NUTIN-UPDATE-REPORT.md';
7
7
 
8
- export async function writeConflictReport(projectPath, conflicts, oldVersion, newVersion) {
9
- const sections = conflicts.map(({ relPath, actualContent, newEntry }) => {
10
- if (newEntry.isBinary || Buffer.isBuffer(actualContent)) {
11
- return `## ${relPath}\n\nBinary file — differs from both your recorded baseline and the new nutin version. Compare manually.\n`;
12
- }
8
+ const list = (items, describe = ({ relPath }) => `\`${relPath}\``) => items.map((item) => `- ${describe(item)}`).join('\n');
13
9
 
14
- const patch = Diff.createTwoFilesPatch(
15
- relPath,
16
- relPath,
17
- actualContent,
18
- newEntry.content,
19
- 'yours',
20
- `nutin v${newVersion}`,
10
+ // Lists the files you edited that this update touched. No diffs: merged files hold both versions'
11
+ // changes, and overlaps are marked in the files themselves.
12
+ export async function writeUpdateReport(projectPath, { merged, conflicted, binaryConflicts }, oldVersion, newVersion) {
13
+ const sections = [
14
+ `# nutin-update report\n\nUpdate from nutin v${oldVersion} to v${newVersion}. These files had changes of yours and changes in the new nutin version.`,
15
+ ];
16
+
17
+ if (conflicted.length > 0) {
18
+ sections.push(
19
+ '## Conflicts to resolve\n\n' +
20
+ 'Your edits and nutin\'s overlap in these files. **The project won\'t build until they\'re resolved.** ' +
21
+ `Each conflict is marked in the file: \`${CONFLICT_MARKER}\` starts your version, \`||||||| nutin v${oldVersion}\` ` +
22
+ `the original one, \`=======\` the new one, ending at \`>>>>>>> nutin v${newVersion}\`. ` +
23
+ 'Keep what you need, delete the markers, then run nutin-update again to confirm.\n\n' +
24
+ list(conflicted, ({ relPath, conflictCount }) => `\`${relPath}\` (${conflictCount} conflict${conflictCount > 1 ? 's' : ''})`),
25
+ );
26
+ }
27
+
28
+ if (merged.length > 0) {
29
+ sections.push(
30
+ '## Merged automatically\n\nYour edits and the new version\'s changes didn\'t overlap, so both were kept. Review them.\n\n' +
31
+ list(merged),
21
32
  );
22
- return `## ${relPath}\n\n\`\`\`diff\n${patch}\`\`\`\n`;
23
- });
33
+ }
24
34
 
25
- const header = '# nutin-update report\n\n' +
26
- `These files changed between nutin v${oldVersion} and v${newVersion}, but you've customized them ` +
27
- 'since generation, so they were left untouched. Review each diff below (yours vs. the new nutin ' +
28
- 'version) and merge by hand.\n\n';
35
+ if (binaryConflicts.length > 0) {
36
+ sections.push(
37
+ '## Binary files left as they are\n\nThese can\'t be merged: your version was kept. Compare it with the new one manually.\n\n' +
38
+ list(binaryConflicts),
39
+ );
40
+ }
29
41
 
30
- await fs.writeFile(path.join(projectPath, REPORT_FILE_NAME), header + sections.join('\n'));
42
+ await fs.writeFile(path.join(projectPath, REPORT_FILE_NAME), sections.join('\n\n') + '\n');
31
43
  }