@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,22 +1,17 @@
1
- import { View, Navigation, Service, I18nService, AppEventBus } from '../../index.js';
2
- import { Language } from '../i18n/languages.js';
3
- import { Routes, RouteConfig, RouteGuardsManager, GuardResult } from './helpers/route-guard-manager.helper.js';
4
- import { ViewRenderManager } from './helpers/view-render-manager.helper.js';
5
- import { NavigationManager } from './helpers/navigation-manager.helper.js';
1
+ import type { View } from '../../base-classes/view/view.js';
2
+ import { Service } from '../../base-classes/service/service.js';
3
+ import { Navigation } from '../event-bus/navigation.facade.js';
4
+ import { AppEventBus } from '../event-bus/event-bus.js';
5
+ import { I18nService } from '../i18n/i18n.js';
6
+ import * as RouteGuardsManager from './helpers/route-guard-manager.helper.js';
7
+ import type { GuardResult } from './helpers/route-guard-manager.helper.js';
8
+ import * as ViewRenderManager from './helpers/view-render-manager.helper.js';
9
+ import * as NavigationManager from './helpers/navigation-manager.helper.js';
10
+ import type { HistoryMode } from './helpers/navigation-manager.helper.js';
11
+ import { CONFIG } from '../../config.js';
6
12
 
7
13
  // centralized export
8
- export { Routes, RouteGuard } from './helpers/route-guard-manager.helper.js';
9
-
10
- /**
11
- * IRouter is a type alias for the instance of Router, not a true interface
12
- */
13
- export type IRouter = InstanceType<typeof Router>;
14
-
15
- export interface RouteMatch {
16
- route: RouteConfig;
17
- params: Record<string, string>;
18
- pattern: string;
19
- }
14
+ export * as NavigationManager from './helpers/navigation-manager.helper.js';
20
15
 
21
16
  class Router extends Service<Router> {
22
17
  private _currentView: View | null = null;
@@ -25,11 +20,16 @@ class Router extends Service<Router> {
25
20
  private onNavigate = (data: { path: string }) => this.navigate(data.path);
26
21
  private onReload = () => this.reload();
27
22
  private _busSubscriptions: Array<() => void> = [];
23
+ // Bumped by every navigation: one whose guards resolve after a newer navigation started
24
+ // is dropped instead of rendering over it.
25
+ private _navId = 0;
26
+ // False until the first view is rendered: the first load leaves focus to the browser.
27
+ private _hasRendered = false;
28
28
 
29
- constructor(private routes: Routes) {
29
+ constructor(private routes: Nutin.Routes) {
30
30
  super();
31
31
  this.initializeEventListeners();
32
- this.navigate(NavigationManager.getCurrentPath());
32
+ this.navigate(this.currentLocation());
33
33
  this.registerCleanup(this.removeEventListeners);
34
34
  }
35
35
 
@@ -41,50 +41,101 @@ class Router extends Service<Router> {
41
41
  window.removeEventListener('popstate', this.onPopState);
42
42
  }
43
43
 
44
+ // Re-renders the current page, so focus isn't moved (see focusView()).
44
45
  public async reload(): Promise<void> {
45
- const currentRoute = NavigationManager.getCurrentPath();
46
- await this.navigate(currentRoute, false);
46
+ await this.navigateWithMode(this.currentLocation(), 'none', false);
47
47
  }
48
48
 
49
49
  public async navigate(path: string | '', pushState: boolean = true): Promise<void> {
50
- const normalizedPath = NavigationManager.normalizePath(path);
50
+ await this.navigateWithMode(path, pushState ? 'push' : 'none', this._hasRendered);
51
+ }
52
+
53
+ public getCurrentParams(): Record<string, string> {
54
+ return { ...this._currentParams };
55
+ }
56
+
57
+ public getParam(key: string): string | undefined {
58
+ return this._currentParams[key];
59
+ }
60
+
61
+ // Path, query and hash of the current URL, as navigate() takes them.
62
+ private currentLocation(): string {
63
+ return NavigationManager.getCurrentPath() + window.location.search + window.location.hash;
64
+ }
65
+
66
+ private async navigateWithMode(path: string, mode: HistoryMode, moveFocus: boolean): Promise<void> {
67
+ const navId = ++this._navId;
68
+ const isStale = () => navId !== this._navId;
69
+ const [pathAndQuery = '', hash] = path.split('#');
70
+ const [rawPath = '', query] = pathAndQuery.split('?');
71
+ const search = query ? `?${query}` : '';
72
+ const normalizedPath = NavigationManager.normalizePath(rawPath);
51
73
  const currentPath = NavigationManager.getCurrentPath();
52
-
74
+
53
75
  // Try to match the route with parameters
54
76
  const routeMatch = this.matchRoute(normalizedPath);
55
77
 
56
78
  if (!routeMatch) {
57
- await this.handleNotFound(normalizedPath, currentPath, pushState);
79
+ await this.handleNotFound(normalizedPath, currentPath, mode, isStale, moveFocus);
58
80
  return;
59
81
  }
60
82
 
61
83
  const guardResult = await this.handleGuards(
62
- normalizedPath,
63
- routeMatch.route,
64
- routeMatch.params,
65
- pushState
84
+ normalizedPath,
85
+ routeMatch.route,
86
+ routeMatch.params,
87
+ currentPath,
88
+ mode,
89
+ moveFocus
66
90
  );
67
-
68
- if (!guardResult) return;
91
+
92
+ if (!guardResult || isStale()) return;
93
+
94
+ // Before the current view is torn down: it stays up while a lazy route's chunk loads.
95
+ const view = await this.resolveView(guardResult.viewConstructor!, NavigationManager.localizedUrl(normalizedPath, search, hash), isStale);
96
+ if (!view) return;
69
97
 
70
98
  this._currentView = await ViewRenderManager.transitionOutCurrentView(this._currentView);
71
99
  this._currentParams = routeMatch.params;
72
- this._currentView = ViewRenderManager.renderNewView(
73
- guardResult.viewConstructor!,
74
- routeMatch.params
75
- );
100
+
101
+ // Must run before renderNewView(): a view's onEnter() may rewrite the URL via NavigationManager.replaceState(),
102
+ // which acts on whatever history entry is current. This ensures current before onEnter() fires.
103
+ NavigationManager.updateHistory(normalizedPath, currentPath, mode, hash, search);
104
+
105
+ this._currentView = ViewRenderManager.renderNewView(view, routeMatch.params);
76
106
 
77
107
  NavigationManager.updateDocumentTitle(this._currentView, routeMatch.pattern);
78
- NavigationManager.updateHistory(normalizedPath, currentPath, pushState);
79
- window.scrollTo({ top: 0 });
108
+ NavigationManager.scrollToHash(hash);
109
+ this.afterRender(moveFocus);
80
110
  }
81
111
 
82
- public getCurrentParams(): Record<string, string> {
83
- return { ...this._currentParams };
112
+ // Runs a route's view factory. A sync one behaves as it always did (its errors propagate).
113
+ // A lazy one is awaited: if it fails to load, recoverFromFailedViewLoad() takes over; if a
114
+ // newer navigation started meanwhile, the view (already mounted, empty, by its
115
+ // constructor) is destroyed and dropped.
116
+ private async resolveView(factory: Nutin.ViewFactory, url: string, isStale: () => boolean): Promise<View | null> {
117
+ if (isStale()) return null;
118
+ const result = factory();
119
+ if (!isPromise(result)) return result;
120
+
121
+ let view: View;
122
+ try {
123
+ view = await result;
124
+ } catch (error) {
125
+ if (!isStale()) NavigationManager.recoverFromFailedViewLoad(url, error);
126
+ return null;
127
+ }
128
+ if (isStale()) {
129
+ view.destroy();
130
+ return null;
131
+ }
132
+ return view;
84
133
  }
85
134
 
86
- public getParam(key: string): string | undefined {
87
- return this._currentParams[key];
135
+ // After updateDocumentTitle(): focusView() announces document.title when the view has no h1.
136
+ private afterRender(moveFocus: boolean): void {
137
+ if (moveFocus && this._currentView) NavigationManager.focusView(this._currentView);
138
+ this._hasRendered = true;
88
139
  }
89
140
 
90
141
  private initializeEventListeners(): void {
@@ -107,11 +158,13 @@ class Router extends Service<Router> {
107
158
  }
108
159
 
109
160
  private async handlePopState(): Promise<void> {
110
- const newLocale = NavigationManager.getCurrentLocale();
111
- if (newLocale && newLocale !== I18nService.currentLanguage) {
112
- await I18nService.setCurrentLanguage(newLocale as Language);
161
+ if (globalThis.__NUTIN_I18N__ ?? CONFIG.i18n) {
162
+ const newLocale = NavigationManager.getCurrentLocale();
163
+ if (newLocale && newLocale !== I18nService.currentLanguage) {
164
+ await I18nService.setCurrentLanguage(newLocale as Nutin.Language);
165
+ }
113
166
  }
114
- this.navigate(NavigationManager.getCurrentPath(), false);
167
+ this.navigateWithMode(this.currentLocation(), 'none', this._hasRendered);
115
168
  }
116
169
 
117
170
  /**
@@ -120,7 +173,7 @@ class Router extends Service<Router> {
120
173
  * - '/users/:id?' matches '/users' and '/users/123'
121
174
  * - '/posts/:id' matches '/posts/123' but not '/posts'
122
175
  */
123
- private matchRoute(path: string): RouteMatch | null {
176
+ private matchRoute(path: string): Nutin.RouteMatch | null {
124
177
  for (const [pattern, routeConfig] of Object.entries(this.routes)) {
125
178
  const match = NavigationManager.matchPattern(pattern, path);
126
179
  if (match) {
@@ -133,7 +186,9 @@ class Router extends Service<Router> {
133
186
  private async handleNotFound(
134
187
  normalizedPath: string,
135
188
  currentPath: string,
136
- pushState: boolean
189
+ mode: HistoryMode,
190
+ isStale: () => boolean,
191
+ moveFocus: boolean
137
192
  ): Promise<void> {
138
193
  const notFoundRoute = this.routes['/404'];
139
194
 
@@ -142,21 +197,26 @@ class Router extends Service<Router> {
142
197
  return;
143
198
  }
144
199
 
145
- const notFoundConstructor = RouteGuardsManager.getViewConstructor(notFoundRoute);
146
-
200
+ const notFoundFactory = RouteGuardsManager.getViewConstructor(notFoundRoute);
201
+ const view = await this.resolveView(notFoundFactory, NavigationManager.localizedUrl(normalizedPath), isStale);
202
+ if (!view) return;
203
+
147
204
  this._currentView = await ViewRenderManager.transitionOutCurrentView(this._currentView);
148
205
  this._currentParams = {};
149
- this._currentView = ViewRenderManager.renderNewView(notFoundConstructor, {});
206
+ this._currentView = ViewRenderManager.renderNewView(view, {});
150
207
 
151
208
  NavigationManager.updateDocumentTitle(this._currentView, '/404');
152
- NavigationManager.updateHistory(normalizedPath, currentPath, pushState);
209
+ NavigationManager.updateHistory(normalizedPath, currentPath, mode);
210
+ this.afterRender(moveFocus);
153
211
  }
154
212
 
155
213
  private async handleGuards(
156
214
  normalizedPath: string,
157
- routeConfig: RouteConfig,
215
+ routeConfig: Nutin.RouteConfig,
158
216
  params: Record<string, string>,
159
- pushState: boolean
217
+ currentPath: string,
218
+ mode: HistoryMode,
219
+ moveFocus: boolean
160
220
  ): Promise<GuardResult | false> {
161
221
  const guardResult = await RouteGuardsManager.processRouteGuards(
162
222
  routeConfig,
@@ -166,7 +226,11 @@ class Router extends Service<Router> {
166
226
 
167
227
  if (!guardResult.allowed) {
168
228
  if (guardResult.redirectTo) {
169
- await this.navigate(guardResult.redirectTo, pushState);
229
+ // A guarded URL that's already the current history entry (back/forward,
230
+ // reload, first load) is replaced by the redirect target, so the address
231
+ // bar matches what's rendered and Back doesn't land on the guarded URL again.
232
+ const redirectMode = mode === 'push' && normalizedPath !== currentPath ? 'push' : 'replace';
233
+ await this.navigateWithMode(guardResult.redirectTo, redirectMode, moveFocus);
170
234
  }
171
235
  // If no redirect, stay on current route (guard blocked navigation)
172
236
  return false;
@@ -176,4 +240,8 @@ class Router extends Service<Router> {
176
240
  }
177
241
  }
178
242
 
179
- export const AppRouter = (routes: Routes) => Router.getInstance(routes);
243
+ function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
244
+ return typeof (value as Promise<T> | null)?.then === 'function';
245
+ }
246
+
247
+ export const AppRouter = (routes: Nutin.Routes) => Router.getInstance(routes);
@@ -2,6 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
6
  <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
7
  </head>
7
8
  <body>
@@ -1,113 +1 @@
1
- @use "variables" as *;
2
-
3
- /* ===========
4
- CSS Reset
5
- =========== */
6
-
7
- /* Box sizing */
8
-
9
- *,
10
- *::before,
11
- *::after {
12
- box-sizing: border-box;
13
- }
14
-
15
- /* Remove default spacing */
16
-
17
- * {
18
- margin: 0;
19
- padding: 0;
20
- }
21
-
22
- /* Document */
23
-
24
- html {
25
- font-size: 100%;
26
- -webkit-text-size-adjust: 100%;
27
- }
28
-
29
- body {
30
- min-height: 100vh;
31
- font: inherit;
32
- line-height: 1.5;
33
- }
34
-
35
- /* Media */
36
-
37
- img,
38
- picture,
39
- svg,
40
- canvas,
41
- video {
42
- display: block;
43
- max-width: 100%;
44
- }
45
-
46
- /* Forms */
47
-
48
- button,
49
- input,
50
- select,
51
- textarea {
52
- font: inherit;
53
- color: inherit;
54
- }
55
-
56
- button {
57
- background: none;
58
- border: none;
59
- cursor: pointer;
60
- }
61
-
62
- textarea {
63
- resize: vertical;
64
- }
65
-
66
- /* Tables */
67
-
68
- table {
69
- border-collapse: collapse;
70
- border-spacing: 0;
71
- }
72
-
73
- /* Lists */
74
-
75
- ul,
76
- ol {
77
- list-style: none;
78
- }
79
-
80
- /* Links */
81
-
82
- a {
83
- color: inherit;
84
- text-decoration: none;
85
- }
86
-
87
- /* Quotes */
88
-
89
- blockquote,
90
- q {
91
- quotes: none;
92
- }
93
-
94
- blockquote::before,
95
- blockquote::after,
96
- q::before,
97
- q::after {
98
- content: "";
99
- }
100
-
101
- /* Miscellaneous */
102
-
103
- hr {
104
- border: none;
105
- }
106
-
107
- fieldset {
108
- border: none;
109
- }
110
-
111
- :where([hidden]) {
112
- display: none !important;
113
- }
1
+ // Global styles
@@ -1,2 +1,2 @@
1
- // CSS reset & global styles
1
+ // Global styles
2
2
  @use "styles";
@@ -19,6 +19,11 @@ function validateConfig(config) {
19
19
 
20
20
  validateConfig(nutinConfig);
21
21
 
22
+ // `build:prod` passes --prod rather than a NODE_ENV=production prefix, which Windows shells
23
+ // don't understand. Every build step is a separate process that only inherits the env (see
24
+ // runScript), so the flag is turned into NODE_ENV here, for them and for this process.
25
+ if (process.argv.includes('--prod')) process.env.NODE_ENV = 'production';
26
+
22
27
  const wellKnownNonSEORoutes = [
23
28
  '/400',
24
29
  '/401',
@@ -34,6 +39,7 @@ export const builderConfig = {
34
39
  ...nutinConfig.builder,
35
40
  isProd: process.env.NODE_ENV === 'production',
36
41
  i18n: nutinConfig.i18n,
42
+ markdownSources: nutinConfig.markdownSources,
37
43
  tailwind: nutinConfig.tailwind,
38
44
  generateSEO: nutinConfig.generateSEOFiles,
39
45
  WELL_KNOWN_NON_SEO_ROUTES: wellKnownNonSEORoutes,
@@ -1,35 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import path from 'path';
4
- import { print, runScript } from '../utils/index.js';
4
+ import { print, runScript, acquireBuildLock } from '../utils/index.js';
5
5
  import { builderConfig } from './builder.config.js';
6
6
 
7
+ // Must happen before copy-static.js wipes/recreates dist-build — a second build
8
+ // starting mid-run (a manual build, another dev watcher instance, ...) would
9
+ // otherwise race it. See tools/utils/build-lock.js.
10
+ await acquireBuildLock();
11
+
7
12
  const scriptsDir = path.join(process.cwd(), 'tools', 'builder', 'core');
8
13
 
9
14
  print.boldHead(`\nStarting build...`);
10
15
 
16
+ // runScript forwards env vars but not argv, so -y/--yes (auto-install an opt-in
17
+ // feature's missing dependencies, see utils/ensure-deps.js) is passed through the env.
18
+ if (process.argv.includes('-y') || process.argv.includes('--yes')) process.env.NUTIN_ASSUME_YES = '1';
19
+
11
20
  runScript(path.join(scriptsDir, 'app', 'copy-static.js'), 'Copying files...');
21
+ {{#if markdown}}
22
+
23
+ runScript(path.join(scriptsDir, 'markdown', 'generate-markdown-pages.js'), 'Compiling Markdown pages...');
24
+ {{/if}}
12
25
 
13
26
  runScript(path.join(scriptsDir, 'html-index', 'validate-html.js'), 'Processing index.html...');
14
27
  runScript(path.join(scriptsDir, 'app', 'validate-routes.js'), 'Validating app routes...');
15
28
 
16
- runScript(path.join(scriptsDir, 'app', 'compile-ts.js'), 'Compiling TypeScript...');
17
-
18
29
  runScript(path.join(scriptsDir, 'html-templates', 'process-html-templates.js'), 'Processing HTML templates...');
30
+ {{#unless (eq lang "js")}}
31
+ // After the templates are merged, so they're type-checked too.
32
+ runScript(path.join(scriptsDir, 'app', 'compile-ts.js'), 'Compiling TypeScript...');
33
+ {{/unless}}
34
+ runScript(path.join(scriptsDir, 'html-templates', 'minify-templates.js'), 'Minifying HTML templates...');
19
35
 
20
36
  runScript(path.join(scriptsDir, 'styles', 'sass.js'), 'Compiling styles...');
21
37
  if (builderConfig.tailwind) runScript(path.join(scriptsDir, 'styles', 'tailwind.js'), 'Compiling Tailwind CSS...');
22
38
 
23
39
  if (builderConfig.i18n) runScript(path.join(scriptsDir, 'i18n', 'build-i18n.js'), 'Combining locales...');
24
40
 
41
+ runScript(path.join(scriptsDir, 'prod-bundle', 'esbuild.js'), 'Running esbuild...');
42
+
25
43
  if (builderConfig.isProd) {
26
- runScript(path.join(scriptsDir, 'prod-bundle', 'esbuild.js'), 'Running esbuild...');
27
- runScript(path.join(scriptsDir, 'prod-bundle', 'hash-files.js'), 'Hashing files...');
28
- runScript(path.join(scriptsDir, 'prod-bundle', 'compress-files.js'), 'Compressing files...');
29
- if (builderConfig.generateSEO) runScript(path.join(scriptsDir, 'seo', 'generate-seo-files.js'), 'Generating SEO Files...');
44
+ runScript(path.join(scriptsDir, 'prod-bundle', 'hash-files.js'), 'Hashing files...');
45
+ if (builderConfig.generateSEO) runScript(path.join(scriptsDir, 'seo', 'generate-seo-files.js'), 'Generating SEO Files...');
46
+ // Last, so the prerendered SEO pages, sitemap.xml and robots.txt get compressed too.
47
+ runScript(path.join(scriptsDir, 'prod-bundle', 'compress-files.js'), 'Compressing files...');
30
48
  }
31
49
 
32
- runScript(path.join(scriptsDir, 'finalize-build.js'), 'Finalizing build...')
50
+ runScript(path.join(scriptsDir, 'finalize-build.js'), 'Finalizing build...');
33
51
 
34
52
  print.boldSuccess(`\nBuild successful!\n`);
35
53
 
@@ -1,14 +1,89 @@
1
- import { runCommand, print, errorExit } from "../../../utils/index.js";
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { captureCommand, errorExit } from "../../../utils/index.js";
2
4
  import { builderConfig } from '../../builder.config.js';
5
+ import { PATHS } from './paths.js';
6
+ import { readTemplateMap } from '../html-templates/merge-templates.js';
7
+ import { remapTscOutput } from './ts-errors.js';
8
+ import { injectHandlerRefs, stripHandlerRefs } from './handler-refs.js';
9
+
10
+ // Runs after the .html templates are merged (process-html-templates), on dist-build's copies,
11
+ // so templates are type-checked like the rest of the code. A tsconfig generated there extends
12
+ // the project's: dist-build/src mirrors src, so its include/exclude apply as written.
13
+ const BUILD_TSCONFIG = path.join(PATHS.temp, 'tsconfig.json');
3
14
 
4
15
  async function compileTS() {
5
- // Prod bundles straight from .ts source (see prod-bundle/esbuild.js), so tsc only
6
- // needs to type-check there, not emit — emitting would leave a stale, unmerged .js
7
- // sibling next to each merged .ts view/component's source.
8
- const args = ['--project', 'tsconfig.json'];
16
+ writeBuildTsconfig();
17
+
18
+ // Prod bundles straight from the merged .ts (see prod-bundle/esbuild.js), so tsc only
19
+ // type-checks there. Dev emits each .js next to its .ts, for the dev bundle and testin-nutin.
20
+ const args = ['--project', BUILD_TSCONFIG, '--pretty', 'false'];
9
21
  if (builderConfig.isProd) args.push('--noEmit');
10
22
 
11
- await runCommand('tsc', args);
23
+ // data-event handlers are type-checked through references injected for tsc only, then
24
+ // removed from the .ts copies the prod bundle is built from (see handler-refs.js).
25
+ const handlerRefs = await injectHandlerRefs();
26
+ const { code, output } = await captureCommand('tsc', args);
27
+ await stripHandlerRefs(handlerRefs);
28
+ const report = remapTscOutput(output, {
29
+ cwd: process.cwd(),
30
+ tempDir: PATHS.temp,
31
+ templateMap: await readTemplateMap(),
32
+ handlerRefs,
33
+ }).trim();
34
+
35
+ // On stdout, where tsc itself writes its diagnostics.
36
+ if (report) console.log(report);
37
+ if (code !== 0) throw new Error(`tsc exited with code ${code}`);
38
+ }
39
+
40
+ function writeBuildTsconfig() {
41
+ const project = readTsconfig(path.resolve('tsconfig.json'));
42
+ const config = {
43
+ extends: '../tsconfig.json',
44
+ compilerOptions: { rootDir: 'src', outDir: 'src' },
45
+ include: (project.include ?? ['src']).map(toBuildPath),
46
+ exclude: (project.exclude ?? []).map(toBuildPath),
47
+ };
48
+ if (project.files) config.files = project.files.map(toBuildPath);
49
+ fs.writeFileSync(BUILD_TSCONFIG, JSON.stringify(config, null, 2));
50
+ }
51
+
52
+ // Paths in src/ are mirrored in dist-build/src; anything else stays where it is, one level up.
53
+ function toBuildPath(entry) {
54
+ const normalized = entry.replace(/^\.\//, '');
55
+ return normalized === 'src' || normalized.startsWith('src/') ? normalized : `../${normalized}`;
56
+ }
57
+
58
+ // tsconfig.json is JSONC: comments and trailing commas are allowed.
59
+ function readTsconfig(file) {
60
+ const source = fs.readFileSync(file, 'utf-8');
61
+ let json = '';
62
+ for (let i = 0; i < source.length; i++) {
63
+ const ch = source[i];
64
+ if (ch === '"') {
65
+ const end = endOfString(source, i);
66
+ json += source.slice(i, end);
67
+ i = end - 1;
68
+ } else if (ch === '/' && source[i + 1] === '/') {
69
+ while (i < source.length && source[i] !== '\n') i++;
70
+ json += '\n';
71
+ } else if (ch === '/' && source[i + 1] === '*') {
72
+ i = source.indexOf('*/', i + 2) + 1;
73
+ if (i === 0) break;
74
+ } else {
75
+ json += ch;
76
+ }
77
+ }
78
+ return JSON.parse(json.replace(/,(\s*[}\]])/g, '$1'));
79
+ }
80
+
81
+ function endOfString(source, start) {
82
+ for (let i = start + 1; i < source.length; i++) {
83
+ if (source[i] === '\\') i++;
84
+ else if (source[i] === '"') return i + 1;
85
+ }
86
+ return source.length;
12
87
  }
13
88
 
14
89
  compileTS().catch((err) => {
@@ -7,7 +7,13 @@ import { builderConfig } from '../../builder.config.js';
7
7
 
8
8
  async function copyStatic() {
9
9
  const extensions = [...BINARY_EXTENSIONS, '.html'];
10
- if (builderConfig.isProd) extensions.push('.ts');
10
+ {{#if (eq lang "js")}}
11
+ // No compile step emits .js here, so sources are copied in dev and prod alike.
12
+ extensions.push('.js');
13
+ {{else}}
14
+ // Templates are merged into these copies, then tsc checks them (and emits .js in dev).
15
+ extensions.push('.ts');
16
+ {{/if}}
11
17
 
12
18
  // Every build starts from a clean dist-build: a failed/interrupted prior
13
19
  // build never gets renamed away by finalize-build.js, so a stale sibling
@@ -23,6 +29,11 @@ async function copyStatic() {
23
29
  const files = await getFilesRecursive(PATHS.source, extension);
24
30
 
25
31
  for (const file of files) {
32
+ {{#if (eq lang "js")}}
33
+ // Tests import the build output; they must not end up in it.
34
+ if (file.endsWith('.test.js')) continue;
35
+
36
+ {{/if}}
26
37
  const relative = path.relative(PATHS.source, file);
27
38
  const dest = path.join(PATHS.tempSource, relative);
28
39