@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,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
  }
@@ -0,0 +1,54 @@
1
+ import path from 'path';
2
+ import * as fsExtra from 'fs-extra';
3
+ import { JsonManager } from '../create-app/json-manager.mjs';
4
+
5
+ const fs = fsExtra.default;
6
+
7
+ // package.json and tsconfig.json aren't rendered from templates (JsonManager writes them
8
+ // once, at creation), so the template diff never sees them. They're compared with what
9
+ // this CLI would generate instead, and only reported.
10
+ // Only the entries nutin generates are compared; anything the user added is ignored.
11
+ const PACKAGE_JSON_SECTIONS = ['scripts', 'devDependencies'];
12
+
13
+ function same(a, b) {
14
+ return JSON.stringify(a) === JSON.stringify(b);
15
+ }
16
+
17
+ function compareEntries(file, section, yours = {}, generated = {}) {
18
+ return Object.entries(generated)
19
+ .filter(([key, value]) => !same(yours[key], value))
20
+ .map(([key, value]) => ({ file, key: section ? `${section}.${key}` : key, yours: yours[key], nutin: value }));
21
+ }
22
+
23
+ async function readJsonIfExists(filePath) {
24
+ if (!(await fs.pathExists(filePath))) return null;
25
+ try {
26
+ return await fs.readJSON(filePath);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export async function findJsonDrift(projectPath, context) {
33
+ const jsonManager = new JsonManager();
34
+ const drift = [];
35
+
36
+ const packageJson = await readJsonIfExists(path.join(projectPath, 'package.json'));
37
+ if (packageJson) {
38
+ const generated = jsonManager.buildPackageJson(context);
39
+ for (const section of PACKAGE_JSON_SECTIONS) {
40
+ drift.push(...compareEntries('package.json', section, packageJson[section], generated[section]));
41
+ }
42
+ }
43
+
44
+ if (context.lang !== 'js') {
45
+ const tsconfig = await readJsonIfExists(path.join(projectPath, 'tsconfig.json'));
46
+ if (tsconfig) {
47
+ const generated = jsonManager.buildTsconfigJson();
48
+ drift.push(...compareEntries('tsconfig.json', 'compilerOptions', tsconfig.compilerOptions, generated.compilerOptions));
49
+ drift.push(...compareEntries('tsconfig.json', null, { include: tsconfig.include, exclude: tsconfig.exclude }, { include: generated.include, exclude: generated.exclude }));
50
+ }
51
+ }
52
+
53
+ return drift;
54
+ }
@@ -8,12 +8,16 @@ import { fetchOldTemplates } from './fetch-old-templates.mjs';
8
8
  import { parseVersion, compareVersions } from './version-compare.mjs';
9
9
  import { bootstrapProjectMeta } from '../common/meta-bootstrap-prompt.mjs';
10
10
  import { printUpdateSummary } from './update-summary-printer.mjs';
11
- import { writeConflictReport } from './conflict-report-writer.mjs';
11
+ import { writeUpdateReport, REPORT_FILE_NAME } from './conflict-report-writer.mjs';
12
+ import { mergeFile, hasConflictMarkers, CONFLICT_MARKER } from './three-way-merge.mjs';
13
+ import { isGitAvailable, getWorkingTreeStatus } from '../common/git.mjs';
12
14
  import { UpdateContextBuilder } from './update-context-builder.mjs';
13
15
  import { TemplateDiffer } from './template-differ.mjs';
16
+ import { findJsonDrift } from './json-drift.mjs';
14
17
 
15
18
  const fs = fsExtra.default;
16
- const REPORT_FILE_NAME = 'NUTIN-UPDATE-REPORT.md';
19
+
20
+ const UNDO_COMMAND = 'git checkout -- . && git clean -fd';
17
21
 
18
22
  export class ProjectUpdater {
19
23
  constructor() {
@@ -22,8 +26,12 @@ export class ProjectUpdater {
22
26
  }
23
27
 
24
28
  async updateProject(projectPath, options = {}) {
25
- const { yes = false, from } = options;
29
+ const { yes = false, from, allowDirty = false } = options;
30
+ // Read before anything here writes .nutin-meta.json (bootstrap, resolved conflicts), so only your
31
+ // own changes count. Enforced right before the first file write.
32
+ const gitStatus = allowDirty ? null : await getWorkingTreeStatus(projectPath);
26
33
  const meta = await this.resolveMeta(projectPath);
34
+ await this.assertNoConflictMarkers(projectPath, meta);
27
35
 
28
36
  if (this.isUpToDate(meta)) {
29
37
  print.boldSuccess(`✅ Already up to date (nutin v${packageVersion}).`);
@@ -37,7 +45,7 @@ export class ProjectUpdater {
37
45
 
38
46
  try {
39
47
  const changeSet = await this.computeChangeSet(projectPath, meta, oldTemplatesRoot);
40
- await this.applyChangeSet(projectPath, meta, changeSet, { yes });
48
+ await this.applyChangeSet(projectPath, meta, changeSet, { yes, gitStatus });
41
49
  } finally {
42
50
  await cleanup();
43
51
  }
@@ -62,6 +70,28 @@ export class ProjectUpdater {
62
70
  }
63
71
  }
64
72
 
73
+ // Files the last update left with conflict markers (meta.unmergedFiles): nothing is updated until
74
+ // they're resolved, so a merge never runs on top of markers and a re-run still reports them.
75
+ async assertNoConflictMarkers(projectPath, meta) {
76
+ const unmerged = meta.unmergedFiles ?? [];
77
+ if (unmerged.length === 0) return;
78
+
79
+ const stillMarked = [];
80
+ for (const relPath of unmerged) {
81
+ if (await hasConflictMarkers(path.join(projectPath, relPath))) stillMarked.push(relPath);
82
+ }
83
+ if (stillMarked.length > 0) {
84
+ throw new Error(
85
+ `${stillMarked.length} file(s) still have conflict markers from the last update — the project won't build until ` +
86
+ `they're resolved. Search for "${CONFLICT_MARKER}", merge, then run nutin-update again:\n` +
87
+ stillMarked.map((relPath) => ` - ${relPath}`).join('\n'),
88
+ );
89
+ }
90
+
91
+ await updateProjectMeta(projectPath, { unmergedFiles: [] });
92
+ print.info('Conflict markers from the last update are resolved.');
93
+ }
94
+
65
95
  isUpToDate(meta) {
66
96
  return compareVersions(meta.version, packageVersion) === 0;
67
97
  }
@@ -84,12 +114,13 @@ export class ProjectUpdater {
84
114
 
85
115
  async computeChangeSet(projectPath, meta, oldTemplatesRoot) {
86
116
  const { oldContext, newContext } = await this.contextBuilder.buildContexts(projectPath, meta);
87
- return this.differ.diff(projectPath, oldTemplatesRoot, oldContext, newContext);
117
+ const changeSet = await this.differ.diff(projectPath, oldTemplatesRoot, oldContext, newContext);
118
+ return { ...changeSet, jsonDrift: await findJsonDrift(projectPath, newContext) };
88
119
  }
89
120
 
90
- async applyChangeSet(projectPath, meta, changeSet, { yes }) {
91
- const { toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated } = changeSet;
92
- printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated });
121
+ async applyChangeSet(projectPath, meta, changeSet, { yes, gitStatus = null }) {
122
+ const { toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated, jsonDrift } = changeSet;
123
+ printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated, jsonDrift });
93
124
 
94
125
  if (toUpdate.length === 0 && toAdd.length === 0 && conflicts.length === 0) {
95
126
  print.boldSuccess('\n✅ Nothing to update.');
@@ -97,18 +128,32 @@ export class ProjectUpdater {
97
128
  return;
98
129
  }
99
130
 
100
- if (!yes && (toUpdate.length > 0 || toAdd.length > 0)) {
131
+ // Binary files can't be merged: they're kept as they are and listed in the report.
132
+ const toMerge = conflicts.filter(({ newEntry, actualContent }) => !newEntry.isBinary && !Buffer.isBuffer(actualContent));
133
+ const binaryConflicts = conflicts.filter((conflict) => !toMerge.includes(conflict));
134
+
135
+ if (gitStatus) this.assertPristine(gitStatus);
136
+
137
+ if (toMerge.length > 0 && !(await isGitAvailable())) {
138
+ throw new Error(
139
+ `nutin-update needs git to merge the ${toMerge.length} file(s) you edited, but git was not found. ` +
140
+ 'Install git and run nutin-update again. No files were changed.',
141
+ );
142
+ }
143
+
144
+ if (!yes && (toUpdate.length > 0 || toAdd.length > 0 || toMerge.length > 0)) {
101
145
  if (!process.stdin.isTTY) {
102
146
  print.warn('\n⚠️ Non-interactive shell detected — skipping the confirmation prompt.');
103
147
  print.boldInfo('Re-run with --yes to apply these updates automatically. No files were changed.');
104
148
  return;
105
149
  }
106
150
 
151
+ const merges = toMerge.length > 0 ? ` and merge ${toMerge.length} file(s) you edited` : '';
107
152
  const { proceed } = await inquirer.prompt([
108
153
  {
109
154
  type: 'confirm',
110
155
  name: 'proceed',
111
- message: `\nApply ${toUpdate.length + toAdd.length} safe file update(s)?`,
156
+ message: `\nApply ${toUpdate.length + toAdd.length} file update(s)${merges}?`,
112
157
  default: true,
113
158
  },
114
159
  ]);
@@ -124,15 +169,79 @@ export class ProjectUpdater {
124
169
  await fs.writeFile(outputPath, newEntry.content);
125
170
  }
126
171
 
127
- if (conflicts.length > 0) {
128
- await writeConflictReport(projectPath, conflicts, meta.version, packageVersion);
129
- print.boldError(`\n⚠️ ${conflicts.length} file(s) you modified need manual attention — see ${REPORT_FILE_NAME}`);
172
+ const { merged, conflicted } = await this.mergeEditedFiles(projectPath, meta, toMerge);
173
+
174
+ if (merged.length > 0 || conflicted.length > 0 || binaryConflicts.length > 0) {
175
+ await writeUpdateReport(projectPath, { merged, conflicted, binaryConflicts }, meta.version, packageVersion);
130
176
  }
131
177
 
132
- await updateProjectMeta(projectPath, { version: packageVersion });
178
+ await updateProjectMeta(projectPath, {
179
+ version: packageVersion,
180
+ unmergedFiles: conflicted.map(({ relPath }) => relPath),
181
+ });
182
+
183
+ if (merged.length > 0) {
184
+ print.info(`\n${merged.length} file(s) you edited were merged with the new version — review them (listed in ${REPORT_FILE_NAME}).`);
185
+ }
186
+ if (binaryConflicts.length > 0) {
187
+ print.boldError(`\n⚠️ ${binaryConflicts.length} binary file(s) you modified were left as they are — see ${REPORT_FILE_NAME}`);
188
+ }
189
+ if (conflicted.length > 0) {
190
+ print.boldError(
191
+ `\n⚠️ ${conflicted.length} file(s) have conflict markers — the project won't build until they're resolved ` +
192
+ `(search for "${CONFLICT_MARKER}"):`,
193
+ );
194
+ conflicted.forEach(({ relPath }) => print.boldError(` - ${relPath}`));
195
+ }
196
+ if (gitStatus) {
197
+ print.info(`\nTo undo this update, run from the project folder: ${UNDO_COMMAND}`);
198
+ print.info(`Important: this will also remove any untracked folder.`);
199
+ }
133
200
 
134
201
  print.boldSuccess(`\n✅ Updated to nutin v${packageVersion}.`);
135
202
  }
203
+
204
+ // The update must be undoable with git: no uncommitted changes and no untracked files in the
205
+ // project folder, so checkout + clean only reverts what the update wrote. --allow-dirty skips this.
206
+ assertPristine({ repo, gitFound, changes }) {
207
+ if (!gitFound) {
208
+ throw new Error('git was not found: nutin-update uses it to make the update undoable. Install git, or pass --allow-dirty. No files were changed.');
209
+ }
210
+ if (!repo) {
211
+ throw new Error(
212
+ 'This project isn\'t in a git repository, so the update couldn\'t be undone. Run "git init" and commit, ' +
213
+ 'or pass --allow-dirty. No files were changed.',
214
+ );
215
+ }
216
+ if (changes.length > 0) {
217
+ throw new Error(
218
+ 'Commit or stash your changes first (untracked files included), so the update can be undone with git, ' +
219
+ `or pass --allow-dirty. No files were changed:\n${changes.map((line) => ` ${line}`).join('\n')}`,
220
+ );
221
+ }
222
+ }
223
+
224
+ // Three-way merge of each file you edited: the old nutin version is the base, so your edits and
225
+ // nutin's changes are both kept, and only overlapping edits get conflict markers.
226
+ async mergeEditedFiles(projectPath, meta, toMerge) {
227
+ const merged = [];
228
+ const conflicted = [];
229
+ const labels = { base: `nutin v${meta.version}`, theirs: `nutin v${packageVersion}` };
230
+
231
+ for (const { relPath, actualContent, baseContent, newEntry } of toMerge) {
232
+ const { content, conflictCount } = await mergeFile({
233
+ base: baseContent,
234
+ yours: actualContent,
235
+ theirs: newEntry.content,
236
+ labels,
237
+ });
238
+ await fs.writeFile(path.join(projectPath, relPath), content);
239
+ if (conflictCount > 0) conflicted.push({ relPath, conflictCount });
240
+ else merged.push({ relPath });
241
+ }
242
+
243
+ return { merged, conflicted };
244
+ }
136
245
  }
137
246
 
138
247
  export const projectUpdater = new ProjectUpdater();
@@ -11,8 +11,8 @@ function contentEquals(a, b) {
11
11
  return a === b;
12
12
  }
13
13
 
14
- // Paths the user is expected to hand-edit right after `create-app` — their generated
15
- // content is just placeholder/starter scaffolding, so nutin-update must never rewrite
14
+ // Paths the user is expected to hand-edit right after `create-app`.
15
+ // Their generated content is just placeholder/starter scaffolding, so nutin-update must never rewrite
16
16
  // or conflict-flag them.
17
17
  const FULLY_EXCLUDED_PATTERNS = [
18
18
  /^config\/[^/]+\.json$/,
@@ -21,7 +21,7 @@ const FULLY_EXCLUDED_PATTERNS = [
21
21
  ];
22
22
 
23
23
  // Like FULLY_EXCLUDED_PATTERNS, but only for files that already existed before this
24
- // update — a brand new file introduced by a newer template version should still be added.
24
+ // update. A brand new file introduced by a newer template version should still be added.
25
25
  const ADD_ONLY_PATTERNS = [
26
26
  /^src\/styles(\/|$)/,
27
27
  ];
@@ -114,8 +114,8 @@ export class TemplateDiffer {
114
114
  if (contentEquals(actualContent, oldEntry.content)) {
115
115
  toUpdate.push({ relPath, newEntry });
116
116
  } else if (!contentEquals(actualContent, newEntry.content)) {
117
- // Differs from both baselines — a real conflict.
118
- conflicts.push({ relPath, actualContent, newEntry });
117
+ // Differs from both baselines: a real conflict, merged from the old baseline at apply time.
118
+ conflicts.push({ relPath, actualContent, baseContent: oldEntry.content, newEntry });
119
119
  }
120
120
  }
121
121
 
@@ -0,0 +1,42 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import * as fsExtra from 'fs-extra';
4
+ import { execFileAsync } from '../common/git.mjs';
5
+
6
+ const fs = fsExtra.default;
7
+
8
+ export const CONFLICT_MARKER = '<<<<<<< yours';
9
+
10
+ // Merges nutin's changes (base -> theirs) into your file with `git merge-file`, which needs the git
11
+ // binary but no repository. Overlapping edits get diff3 markers: yours, the old nutin version, the new one.
12
+ export async function mergeFile({ base, yours, theirs, labels }) {
13
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nutin-merge-'));
14
+ try {
15
+ const [yoursPath, basePath, theirsPath] = ['yours', 'base', 'theirs'].map((name) => path.join(tmpDir, name));
16
+ await Promise.all([
17
+ fs.writeFile(yoursPath, yours),
18
+ fs.writeFile(basePath, base),
19
+ fs.writeFile(theirsPath, theirs),
20
+ ]);
21
+
22
+ const args = ['merge-file', '-p', '--diff3', '-L', 'yours', '-L', labels.base, '-L', labels.theirs, yoursPath, basePath, theirsPath];
23
+ try {
24
+ const { stdout } = await execFileAsync('git', args, { maxBuffer: 1024 * 1024 * 50 });
25
+ return { content: stdout, conflictCount: 0 };
26
+ } catch (error) {
27
+ // A positive exit code below 255 is the number of conflicts; anything else is a real failure.
28
+ if (Number.isInteger(error.code) && error.code > 0 && error.code < 255) {
29
+ return { content: error.stdout, conflictCount: error.code };
30
+ }
31
+ throw error;
32
+ }
33
+ } finally {
34
+ await fs.remove(tmpDir);
35
+ }
36
+ }
37
+
38
+ export async function hasConflictMarkers(filePath) {
39
+ if (!(await fs.pathExists(filePath))) return false;
40
+ const content = await fs.readFile(filePath, 'utf8');
41
+ return content.split('\n').some((line) => line.startsWith(CONFLICT_MARKER));
42
+ }
@@ -13,6 +13,7 @@ export class UpdateContextBuilder {
13
13
  const baseContext = {
14
14
  projectName: packageJson.name,
15
15
  packageManager,
16
+ lang: meta.lang ?? 'ts',
16
17
  ...meta.features,
17
18
  ciCommand: getCiCommand(packageManager),
18
19
  };
@@ -1,11 +1,13 @@
1
1
  import { print } from '../utils/print.mjs';
2
2
 
3
- export function printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated }) {
3
+ const describeValue = (value) => (value === undefined ? '(missing)' : JSON.stringify(value));
4
+
5
+ export function printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, removedByUser, noLongerGenerated, jsonDrift = [] }) {
4
6
  print.section('\nSummary:');
5
7
  print.info(` ${toUpdate.length} file(s) will be updated (untouched since generation)`);
6
8
  print.info(` ${toAdd.length} new file(s) will be added`);
7
9
  if (conflicts.length > 0) {
8
- print.boldError(` ${conflicts.length} file(s) you modified need manual attention`);
10
+ print.boldError(` ${conflicts.length} file(s) you edited will be merged with the new version`);
9
11
  }
10
12
  if (noLongerGenerated.length > 0) {
11
13
  print.boldError(` ${noLongerGenerated.length} file(s) nutin no longer generates — left in place, review manually:`);
@@ -19,4 +21,10 @@ export function printUpdateSummary({ toUpdate, toAdd, conflicts, unknown, remove
19
21
  print.section(` ${removedByUser.length} file(s) nutin used to generate but you removed — not recreated:`);
20
22
  removedByUser.forEach(({ relPath }) => print.section(` - ${relPath}`));
21
23
  }
24
+ if (jsonDrift.length > 0) {
25
+ print.section(` ${jsonDrift.length} package.json/tsconfig.json value(s) differ from what this nutin version generates — never changed automatically, apply by hand if wanted:`);
26
+ jsonDrift.forEach(({ file, key, yours, nutin }) =>
27
+ print.section(` - ${file} ${key}: yours ${describeValue(yours)}, nutin ${describeValue(nutin)}`)
28
+ );
29
+ }
22
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nutin/cli",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "The structure of a framework. The freedom of vanilla.",
5
5
  "preferGlobal": true,
6
6
  "main": "lib/index.mjs",
@@ -17,7 +17,7 @@
17
17
  "minor": "npm version minor -m 'CI/CD: Bump version to %s'",
18
18
  "major": "npm version major -m 'CI/CD: Bump version to %s'",
19
19
  "test": "echo \"Error: no test specified\" && exit 1",
20
- "publish": "npm publish --access public"
20
+ "release": "npm publish --access public"
21
21
  },
22
22
  "keywords": [
23
23
  "Nutin",
@@ -30,22 +30,23 @@
30
30
  "author": "Loïs Kouninef <loiskouninef@gmail.com>",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "commander": "^9.0.0",
33
+ "commander": "^15.0.0",
34
34
  "diff": "^9.0.0",
35
- "fs-extra": "^11.0.0",
36
- "handlebars": "^4.7.8",
37
- "inquirer": "^9.0.0",
38
- "tar": "^7.5.22"
35
+ "fs-extra": "^11.4.1",
36
+ "handlebars": "^4.7.10",
37
+ "inquirer": "^14.2.3",
38
+ "tar": "^7.5.22",
39
+ "typescript": "5.9.3"
39
40
  },
40
41
  "engines": {
41
- "node": ">=22"
42
+ "node": ">=24"
42
43
  },
43
44
  "repository": {
44
45
  "type": "git",
45
- "url": "https://github.com/loiskouninef/nutin.git"
46
+ "url": "git+https://github.com/LoisKOUNINEF/nutin.git"
46
47
  },
47
48
  "bugs": {
48
- "url": "https://github.com/loiskouninef/nutin/issues"
49
+ "url": "https://github.com/LoisKOUNINEF/nutin/issues"
49
50
  },
50
- "homepage": "www.nutin.org"
51
+ "homepage": "www.nutin.dev"
51
52
  }
@@ -7,6 +7,7 @@ pnpm-debug.log*
7
7
 
8
8
  /dist/
9
9
  /dist-build/
10
+ /.build-lock/
10
11
  /coverage/
11
12
 
12
13
  # Generated by "validate-docker" (run via docker:build) from tools/docker/*.template — only present when the docker feature is added