@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
@@ -0,0 +1,114 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getFilesRecursive } from '../../../utils/index.js';
4
+ import { findInlineTemplate } from '../html-templates/template-literal.js';
5
+ import { PATHS } from './paths.js';
6
+
7
+ // data-event handlers are looked up by name at runtime, so tsc can't see them: a typo binds
8
+ // nothing, and noUnusedLocals flags every private handler. Before tsc, each component/view's
9
+ // handler names are referenced from its own class, on the class's opening line (no line shifts):
10
+ // class TaskCardComponent extends Component { static { void ((c: InstanceType<typeof TaskCardComponent>) => [c._save]); }
11
+ // A class body may read its own private members, so a missing handler is a tsc error and an
12
+ // existing one counts as used. The arrow is never called; stripHandlerRefs() removes it after tsc.
13
+
14
+ const DATA_EVENT = /\bdata-event\s*=\s*(["'])(.*?)\1/gs;
15
+ const CLASS_OPEN = /\bclass\s+([A-Za-z_$][\w$]*)\b[^{;]*?\bextends\b[^{;]*\{/g;
16
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
17
+
18
+ // Injects the references into every .ts under src/app; returns where they went, by path
19
+ // relative to dist-build (as ts-errors.js receives tsc's paths).
20
+ export async function injectHandlerRefs() {
21
+ const refs = {};
22
+ for (const codePath of await getFilesRecursive(PATHS.tempApp, ['.ts'])) {
23
+ const content = await fs.readFile(codePath, 'utf-8');
24
+ const result = addHandlerRefs(content);
25
+ if (!result) continue;
26
+ await fs.writeFile(codePath, result.content);
27
+ refs[toPosix(path.relative(PATHS.temp, codePath))] = result.entry;
28
+ }
29
+ return refs;
30
+ }
31
+
32
+ export async function stripHandlerRefs(refs) {
33
+ for (const [relPath, entry] of Object.entries(refs)) {
34
+ const codePath = path.join(PATHS.temp, relPath);
35
+ const content = await fs.readFile(codePath, 'utf-8');
36
+ await fs.writeFile(codePath, removeHandlerRefs(content, entry));
37
+ }
38
+ }
39
+
40
+ // The template's handler names, referenced from the class that follows (or precedes) it.
41
+ // Returns null when there's nothing to check. entry: { className, line, col, segment, handlers }
42
+ // with 1-based positions; each handler's col is where it sits in the injected line, and its
43
+ // templateLine/templateCol where its name is written in the template.
44
+ export function addHandlerRefs(content) {
45
+ const template = findInlineTemplate(content);
46
+ if (!template || template.body === null) return null;
47
+
48
+ const handlers = collectHandlers(content, template);
49
+ if (!handlers.length) return null;
50
+
51
+ const classOpen = findClassOpen(content, template);
52
+ if (!classOpen) return null;
53
+
54
+ const prefix = ` static { void ((c: InstanceType<typeof ${classOpen.name}>) => [`;
55
+ let segment = prefix;
56
+ const { line, col } = positionOf(content, classOpen.end);
57
+ const located = handlers.map((handler, i) => {
58
+ if (i > 0) segment += ', ';
59
+ const handlerCol = col + segment.length + 2; // past "c."
60
+ segment += `c.${handler.name}`;
61
+ return { ...handler, col: handlerCol };
62
+ });
63
+ segment += ']); }';
64
+
65
+ return {
66
+ content: content.slice(0, classOpen.end) + segment + content.slice(classOpen.end),
67
+ entry: { className: classOpen.name, line, col, segment, handlers: located },
68
+ };
69
+ }
70
+
71
+ export function removeHandlerRefs(content, entry) {
72
+ const index = content.indexOf(entry.segment);
73
+ return index === -1 ? content : content.slice(0, index) + content.slice(index + entry.segment.length);
74
+ }
75
+
76
+ // Each handler named in a static data-event (one built with ${} is skipped), once, at its
77
+ // first occurrence.
78
+ function collectHandlers(content, template) {
79
+ const handlers = new Map();
80
+ for (const match of template.body.matchAll(DATA_EVENT)) {
81
+ const value = match[2];
82
+ if (value.includes('${')) continue;
83
+ const name = value.split(':')[1]?.trim();
84
+ if (!name || !IDENTIFIER.test(name) || handlers.has(name)) continue;
85
+
86
+ const valueStart = match.index + match[0].indexOf(match[1]) + 1;
87
+ const afterColon = value.indexOf(':') + 1;
88
+ const nameOffset = template.open + valueStart + afterColon + value.slice(afterColon).indexOf(name);
89
+ const { line, col } = positionOf(content, nameOffset);
90
+ handlers.set(name, { name, templateLine: line, templateCol: col });
91
+ }
92
+ return [...handlers.values()];
93
+ }
94
+
95
+ // The first `class X ... extends ... {` outside the template: after it first, else before it.
96
+ function findClassOpen(content, template) {
97
+ const end = template.end ?? template.open;
98
+ let found = null;
99
+ for (const match of content.matchAll(CLASS_OPEN)) {
100
+ const inTemplate = match.index >= template.open && match.index < end;
101
+ if (inTemplate) continue;
102
+ const candidate = { name: match[1], end: match.index + match[0].length };
103
+ if (match.index >= end) return candidate;
104
+ found ??= candidate;
105
+ }
106
+ return found;
107
+ }
108
+
109
+ function positionOf(content, offset) {
110
+ const before = content.slice(0, offset);
111
+ return { line: before.split('\n').length, col: offset - before.lastIndexOf('\n') };
112
+ }
113
+
114
+ const toPosix = (p) => p.split(path.sep).join('/');
@@ -0,0 +1,70 @@
1
+ import { addHandlerRefs, removeHandlerRefs } from './handler-refs.js';
2
+
3
+ const VIEW = [
4
+ "import { View, html } from '../../../core/index.js';",
5
+ '',
6
+ 'const template = html`<div>',
7
+ ' <button data-event="click:_save:@value">Save</button>',
8
+ ' <a data-event=\'click:_open\' data-event-extra="x">Open</a>',
9
+ ' <button data-event="click:_save">Again</button>',
10
+ '</div>`;',
11
+ '',
12
+ 'export class TaskView extends View {',
13
+ ' private _save(): void {}',
14
+ ' private _open(): void {}',
15
+ '}',
16
+ ].join('\n');
17
+
18
+ describe('handler-refs', () => {
19
+ it('references each data-event handler once, from the class\'s opening line', () => {
20
+ const { content, entry } = addHandlerRefs(VIEW);
21
+ const classLine = content.split('\n')[8];
22
+
23
+ expect(classLine).toBe(
24
+ 'export class TaskView extends View { static { void ((c: InstanceType<typeof TaskView>) => [c._save, c._open]); }'
25
+ );
26
+ expect(content.split('\n').length).toBe(VIEW.split('\n').length);
27
+ expect(entry.className).toBe('TaskView');
28
+ expect(entry.line).toBe(9);
29
+ expect(entry.handlers.map((h) => h.name)).toEqual(['_save', '_open']);
30
+ });
31
+
32
+ it('records where each handler is named in the template and in the injected line', () => {
33
+ const { content, entry } = addHandlerRefs(VIEW);
34
+ const [save, open] = entry.handlers;
35
+
36
+ expect(save.templateLine).toBe(4);
37
+ expect(VIEW.split('\n')[3].slice(save.templateCol - 1).startsWith('_save:@value')).toBeTruthy();
38
+ expect(open.templateLine).toBe(5);
39
+ expect(VIEW.split('\n')[4].slice(open.templateCol - 1).startsWith('_open\'')).toBeTruthy();
40
+
41
+ const classLine = content.split('\n')[8];
42
+ expect(classLine.slice(save.col - 1).startsWith('_save,')).toBeTruthy();
43
+ expect(classLine.slice(open.col - 1).startsWith('_open]')).toBeTruthy();
44
+ });
45
+
46
+ it('skips data-event values built with ${}, and files without handlers', () => {
47
+ const dynamic = VIEW
48
+ .replace('click:_save:@value', 'click:${handler}')
49
+ .replace("'click:_open'", "'click:${other}'")
50
+ .replace('"click:_save"', '"${evt}:_save"');
51
+ expect(addHandlerRefs(dynamic)).toBe(null);
52
+ expect(addHandlerRefs("export class A extends View {}")).toBe(null);
53
+ });
54
+
55
+ it('finds the class after the template, even when the markup mentions a class', () => {
56
+ const source = [
57
+ 'const templateFn = (t: T) => html`<p class="x">the class Foo extends Bar { }</p><i data-event="click:_go"></i>`;',
58
+ 'export class CardComponent<T = string> extends Component<T> {',
59
+ '}',
60
+ ].join('\n');
61
+ const { content, entry } = addHandlerRefs(source);
62
+ expect(entry.className).toBe('CardComponent');
63
+ expect(content.split('\n')[1]).toContain('extends Component<T> { static { void ((c: InstanceType<typeof CardComponent>) => [c._go]); }');
64
+ });
65
+
66
+ it('removes exactly the injected references', () => {
67
+ const { content, entry } = addHandlerRefs(VIEW);
68
+ expect(removeHandlerRefs(content, entry)).toBe(VIEW);
69
+ });
70
+ });
@@ -4,6 +4,7 @@ const SRC_FOLDERNAME = 'src';
4
4
  const APP_FOLDERNAME = 'app';
5
5
  const TMP_FOLDERNAME = 'dist-build';
6
6
  const BLD_FOLDERNAME = 'dist';
7
+ const CHUNKS_FOLDERNAME = 'chunks';
7
8
 
8
9
  const BASE_PATHS = {
9
10
  source: path.resolve(SRC_FOLDERNAME),
@@ -16,4 +17,20 @@ export const PATHS = {
16
17
  sourceApp: path.join(BASE_PATHS.source, APP_FOLDERNAME),
17
18
  tempSource: path.join(BASE_PATHS.temp, SRC_FOLDERNAME),
18
19
  tempApp: path.join(BASE_PATHS.temp, SRC_FOLDERNAME, APP_FOLDERNAME),
20
+ tempChunks: path.join(BASE_PATHS.temp, SRC_FOLDERNAME, CHUNKS_FOLDERNAME),
19
21
  };
22
+
23
+ // Copied sources that only feed the bundles — finalize-build.js removes them in prod,
24
+ // so hashing/compressing them is wasted work (and renaming them breaks the SSR bundle).
25
+ const BUNDLED_SOURCE_DIRS = [PATHS.tempApp, path.join(PATHS.tempSource, 'core')];
26
+
27
+ export function isBundledSource(file) {
28
+ return BUNDLED_SOURCE_DIRS.some((dir) => file.startsWith(dir + path.sep));
29
+ }
30
+
31
+ // esbuild's code-split chunks (dynamic import()s and the code they share). Already
32
+ // content-hashed by esbuild, and imported by name from bundle.js and from each other,
33
+ // so hash-files.js must not rename them.
34
+ export function isChunk(file) {
35
+ return file.startsWith(PATHS.tempChunks + path.sep);
36
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import esbuild from 'esbuild';
4
+ import { getFilesRecursive } from '../../../utils/index.js';
5
+
6
+ const DUPLICATE_KEY_TEXT = /^Duplicate key "(.*)" in object literal$/;
7
+
8
+ // esbuild warns on every duplicate key of an object literal — appRoutes' included — and
9
+ // treats '/x', "/x" and ['/x'] as the same key. Scope is the whole file, not only appRoutes.
10
+ // Rejects (with esbuild's own message) when the source doesn't parse.
11
+ export async function findDuplicateRouteKeys(source, { loader, sourcefile }) {
12
+ const { warnings } = await esbuild.transform(source, { loader, sourcefile });
13
+ const duplicates = new Map();
14
+
15
+ for (const warning of warnings) {
16
+ if (warning.id !== 'duplicate-object-key') continue;
17
+ const key = warning.text.match(DUPLICATE_KEY_TEXT)?.[1] ?? warning.text;
18
+ if (!duplicates.has(key)) duplicates.set(key, warning.location?.line);
19
+ }
20
+
21
+ return [...duplicates].map(([key, line]) => ({ key, line }));
22
+ }
23
+
24
+ // Static, bare and dynamic imports: from '…', import '…', import('…').
25
+ const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*)(['"`])([^'"`]+)\1/g;
26
+ const CODE_EXTENSION = /\.(ts|js)$/;
27
+
28
+ // The app files that import the routes module. Without one, the app doesn't use the router
29
+ // (e.g. components mounted into a page), so routes.ts and #app aren't required.
30
+ // files: [{ path, content }], absolute paths; routesFile: the routes module's absolute path.
31
+ export function findRoutesImporters(files, routesFile) {
32
+ const routesModule = routesFile.replace(CODE_EXTENSION, '');
33
+ return files
34
+ .filter(({ path: file, content }) => file !== routesFile && [...content.matchAll(IMPORT_SPECIFIER)].some(
35
+ ([, , specifier]) => specifier.startsWith('.') &&
36
+ path.resolve(path.dirname(file), specifier).replace(CODE_EXTENSION, '') === routesModule
37
+ ))
38
+ .map(({ path: file }) => file);
39
+ }
40
+
41
+ // findRoutesImporters() over the app's code files (tests and .d.ts left out).
42
+ export function readRoutesImporters(appDir, routesFile) {
43
+ const files = getFilesRecursive(appDir, ['.ts', '.js'])
44
+ .filter((file) => !/\.test\.(ts|js)$/.test(file) && !file.endsWith('.d.ts'))
45
+ .map((file) => ({ path: file, content: fs.readFileSync(file, 'utf-8') }));
46
+ return findRoutesImporters(files, routesFile);
47
+ }
@@ -0,0 +1,97 @@
1
+ import path from 'path';
2
+ import { findDuplicateRouteKeys, findRoutesImporters } from './route-keys.js';
3
+
4
+ const TS = { loader: 'ts', sourcefile: 'routes.ts' };
5
+
6
+ describe('findDuplicateRouteKeys', () => {
7
+ it('returns nothing for distinct keys, TypeScript syntax included', async () => {
8
+ const source = `
9
+ export const appRoutes: Nutin.Routes = {
10
+ '/': () => new HomeView(),
11
+ '/about': () => new AboutView() as any,
12
+ '/users/:id': () => new UserView(),
13
+ };`;
14
+
15
+ expect(await findDuplicateRouteKeys(source, TS)).toEqual([]);
16
+ });
17
+
18
+ it('reports a duplicate across quote styles and computed keys, with its line', async () => {
19
+ const source = [
20
+ 'export const appRoutes: Nutin.Routes = {',
21
+ " '/': () => new HomeView(),",
22
+ ' "/": () => new OtherView(),',
23
+ " ['/c']: () => new C(),",
24
+ " ['/c']: () => new D(),",
25
+ '};',
26
+ ].join('\n');
27
+
28
+ expect(await findDuplicateRouteKeys(source, TS)).toEqual([
29
+ { key: '/', line: 3 },
30
+ { key: '/c', line: 5 },
31
+ ]);
32
+ });
33
+
34
+ it('reports a duplicated :param route', async () => {
35
+ const source = `export const appRoutes = {
36
+ '/users/:id': () => new A(),
37
+ '/users/:id': () => new B(),
38
+ };`;
39
+
40
+ expect((await findDuplicateRouteKeys(source, { loader: 'js', sourcefile: 'routes.js' })).map((d) => d.key)).toEqual(['/users/:id']);
41
+ });
42
+
43
+ it('does not flag the view/guards keys shared by several guarded routes', async () => {
44
+ const source = `export const appRoutes: Nutin.Routes = {
45
+ '/admin': { view: () => new AdminView(), guards: [() => true] },
46
+ '/settings': { view: () => new SettingsView(), guards: [() => '/'] },
47
+ };`;
48
+
49
+ expect(await findDuplicateRouteKeys(source, TS)).toEqual([]);
50
+ });
51
+
52
+ it('flags a plain and a guarded route sharing a path', async () => {
53
+ const source = `export const appRoutes: Nutin.Routes = {
54
+ '/admin': () => new AdminView(),
55
+ '/admin': { view: () => new AdminView(), guards: [() => true] },
56
+ };`;
57
+
58
+ expect((await findDuplicateRouteKeys(source, TS)).map((d) => d.key)).toEqual(['/admin']);
59
+ });
60
+
61
+ it('checks the whole file, not only appRoutes', async () => {
62
+ const source = `const other = { a: 1, a: 2 };
63
+ export const appRoutes: Nutin.Routes = { '/': () => new HomeView() };`;
64
+
65
+ expect(await findDuplicateRouteKeys(source, TS)).toEqual([{ key: 'a', line: 1 }]);
66
+ });
67
+
68
+ it('rejects when the source does not parse', async () => {
69
+ await expect(() => findDuplicateRouteKeys('export const appRoutes = {', TS)).toThrow();
70
+ });
71
+ });
72
+
73
+ describe('findRoutesImporters', () => {
74
+ const APP = path.resolve('/project/src/app');
75
+ const ROUTES = path.join(APP, 'routes.ts');
76
+ const file = (rel, content) => ({ path: path.join(APP, rel), content });
77
+
78
+ it('finds static, bare and dynamic imports of the routes module, from any folder', () => {
79
+ const files = [
80
+ file('main.ts', "import { appRoutes } from './routes.js';"),
81
+ file('setup.ts', "import './routes.js';"),
82
+ file('lazy/boot.ts', "const r = await import('../routes.js');"),
83
+ ];
84
+ expect(findRoutesImporters(files, ROUTES)).toEqual(files.map((f) => f.path));
85
+ });
86
+
87
+ it('ignores other modules named routes, packages, and the routes file itself', () => {
88
+ const files = [
89
+ file('main.ts', "import { CounterComponent } from './components/counter/counter.component.js';"),
90
+ file('admin/main.ts', "import { adminRoutes } from './routes.js';"),
91
+ file('x.ts', "import routes from 'routes.js';"),
92
+ file('routes.ts', "import { markdownRoutes } from './markdown/markdown-routes.js';"),
93
+ ];
94
+ expect(findRoutesImporters(files, ROUTES)).toEqual([]);
95
+ });
96
+ });
97
+
@@ -0,0 +1,58 @@
1
+ import path from 'path';
2
+
3
+ // tsc reports errors in dist-build's merged copies. This maps each `file(line,col): …` line
4
+ // back to the project's own file: the .html template when the position falls inside a merged
5
+ // template, the source .ts otherwise (its lines after the template shifted back up).
6
+ // templateMap: { 'src/…/x.component.ts': { html, line, col, lines } } (see merge-templates)
7
+ // handlerRefs: { 'src/…/x.component.ts': { className, line, col, segment, handlers } } (see handler-refs):
8
+ // an error in the injected handler references is reported where the handler is named in the template.
9
+ const LOCATION = /^(.+?)\((\d+),(\d+)\)(: .*)$/;
10
+
11
+ export function remapTscOutput(output, { cwd, tempDir, templateMap, handlerRefs = {} }) {
12
+ return output
13
+ .split('\n')
14
+ .map((line) => {
15
+ const match = LOCATION.exec(line);
16
+ if (!match) return line;
17
+ const [, file, lineNo, colNo, rest] = match;
18
+ const absolute = path.resolve(cwd, file);
19
+ if (!absolute.startsWith(tempDir + path.sep)) return line;
20
+
21
+ const relPath = path.relative(tempDir, absolute);
22
+ const handlerRef = mapHandlerRef(handlerRefs[toPosix(relPath)], Number(lineNo), Number(colNo));
23
+ const { file: mappedFile, line: mappedLine, col: mappedCol } = mapPosition(relPath, handlerRef.line, handlerRef.col, templateMap);
24
+ // A private handler declared in a parent class: TS2341 names the parent; say what to do.
25
+ const hint = handlerRef.note && rest.includes('TS2341') ? ' Declare it protected to use it in a subclass template.' : '';
26
+ return `${toPosix(mappedFile)}(${mappedLine},${mappedCol})${rest}${handlerRef.note}${hint}`;
27
+ })
28
+ .join('\n');
29
+ }
30
+
31
+ export function mapPosition(relPath, line, col, templateMap) {
32
+ const entry = templateMap[toPosix(relPath)] ?? templateMap[relPath];
33
+ if (!entry || line < entry.line) return { file: relPath, line, col };
34
+
35
+ const lastTemplateLine = entry.line + entry.lines - 1;
36
+ if (line > lastTemplateLine) return { file: relPath, line: line - (entry.lines - 1), col };
37
+
38
+ // Inside the template: its first line starts where the placeholder was.
39
+ const htmlLine = line - entry.line + 1;
40
+ return { file: entry.html, line: htmlLine, col: htmlLine === 1 ? col - entry.col + 1 : col };
41
+ }
42
+
43
+ // A position on a line where handler references were injected: inside them, the handler's place
44
+ // in the template (or the class, for an error about the injection itself); after them, shifted back.
45
+ export function mapHandlerRef(entry, line, col) {
46
+ if (!entry || line !== entry.line || col < entry.col) return { line, col, note: '' };
47
+ if (col >= entry.col + entry.segment.length) return { line, col: col - entry.segment.length, note: '' };
48
+
49
+ const handler = entry.handlers.find((h) => col >= h.col && col < h.col + h.name.length);
50
+ if (!handler) return { line, col: Math.max(entry.col - 1, 1), note: ` (data-event handlers of ${entry.className})` };
51
+ return {
52
+ line: handler.templateLine,
53
+ col: handler.templateCol,
54
+ note: ` (data-event handler "${handler.name}" of ${entry.className})`,
55
+ };
56
+ }
57
+
58
+ const toPosix = (p) => p.split(path.sep).join('/');
@@ -0,0 +1,84 @@
1
+ import path from 'path';
2
+ import { remapTscOutput, mapPosition, mapHandlerRef } from './ts-errors.js';
3
+
4
+ // A view whose placeholder sat on line 4, column 33, replaced by a 3-line .html template.
5
+ const MAP = {
6
+ 'src/app/views/home/home.view.ts': { html: 'src/app/views/home/home.view.html', line: 4, col: 33, lines: 3 },
7
+ };
8
+
9
+ describe('ts-errors', () => {
10
+ it('maps a position before the template to the same line of the source file', () => {
11
+ expect(mapPosition('src/app/views/home/home.view.ts', 2, 5, MAP)).toEqual({ file: 'src/app/views/home/home.view.ts', line: 2, col: 5 });
12
+ });
13
+
14
+ it('maps a position inside the template to the .html file, the first line\'s column shifted', () => {
15
+ expect(mapPosition('src/app/views/home/home.view.ts', 4, 40, MAP)).toEqual({ file: 'src/app/views/home/home.view.html', line: 1, col: 8 });
16
+ expect(mapPosition('src/app/views/home/home.view.ts', 6, 12, MAP)).toEqual({ file: 'src/app/views/home/home.view.html', line: 3, col: 12 });
17
+ });
18
+
19
+ it('shifts positions after the template back up by the lines it added', () => {
20
+ expect(mapPosition('src/app/views/home/home.view.ts', 9, 3, MAP)).toEqual({ file: 'src/app/views/home/home.view.ts', line: 7, col: 3 });
21
+ });
22
+
23
+ it('leaves files without a merged template as they are', () => {
24
+ expect(mapPosition('src/core/index.ts', 9, 3, MAP)).toEqual({ file: 'src/core/index.ts', line: 9, col: 3 });
25
+ });
26
+
27
+ it('rewrites tsc lines from dist-build to the project\'s files and leaves other lines alone', () => {
28
+ const cwd = path.resolve('/project');
29
+ const tempDir = path.join(cwd, 'dist-build');
30
+ const output = [
31
+ "dist-build/src/app/views/home/home.view.ts(5,9): error TS2339: Property 'nmae' does not exist on type 'ITask'.",
32
+ 'dist-build/src/core/index.ts(1,1): error TS1000: Something.',
33
+ ' continued message line',
34
+ "node_modules/x/index.d.ts(3,1): error TS2000: Elsewhere.",
35
+ ].join('\n');
36
+
37
+ expect(remapTscOutput(output, { cwd, tempDir, templateMap: MAP }).split('\n')).toEqual([
38
+ "src/app/views/home/home.view.html(2,9): error TS2339: Property 'nmae' does not exist on type 'ITask'.",
39
+ 'src/core/index.ts(1,1): error TS1000: Something.',
40
+ ' continued message line',
41
+ "node_modules/x/index.d.ts(3,1): error TS2000: Elsewhere.",
42
+ ]);
43
+ });
44
+
45
+ // Handler references injected on line 9 from column 31 (see handler-refs.js): "_svae" sits at
46
+ // column 80 of that line, and is written on line 5, column 26 of the merged template.
47
+ const SEGMENT = ' static { void ((c: InstanceType<typeof HomeView>) => [c._svae]); }';
48
+ const REFS = {
49
+ 'src/app/views/home/home.view.ts': {
50
+ className: 'HomeView', line: 9, col: 31, segment: SEGMENT,
51
+ handlers: [{ name: '_svae', col: 31 + SEGMENT.indexOf('_svae'), templateLine: 5, templateCol: 26 }],
52
+ },
53
+ };
54
+
55
+ it('maps an error on an injected handler reference to where the template names it', () => {
56
+ const entry = REFS['src/app/views/home/home.view.ts'];
57
+ const col = entry.handlers[0].col;
58
+ expect(mapHandlerRef(entry, 9, col)).toEqual({ line: 5, col: 26, note: ' (data-event handler "_svae" of HomeView)' });
59
+ expect(mapHandlerRef(entry, 9, col + 4).line).toBe(5);
60
+ });
61
+
62
+ it('shifts positions after the injected references back, and leaves other lines alone', () => {
63
+ const entry = REFS['src/app/views/home/home.view.ts'];
64
+ expect(mapHandlerRef(entry, 9, 31 + SEGMENT.length + 2)).toEqual({ line: 9, col: 33, note: '' });
65
+ expect(mapHandlerRef(entry, 9, 12)).toEqual({ line: 9, col: 12, note: '' });
66
+ expect(mapHandlerRef(entry, 10, 40)).toEqual({ line: 10, col: 40, note: '' });
67
+ expect(mapHandlerRef(undefined, 9, 40)).toEqual({ line: 9, col: 40, note: '' });
68
+ });
69
+
70
+ it('reports a handler error at the .html line, with a hint for a parent\'s private handler', () => {
71
+ const cwd = path.resolve('/project');
72
+ const tempDir = path.join(cwd, 'dist-build');
73
+ const col = REFS['src/app/views/home/home.view.ts'].handlers[0].col;
74
+ const output = [
75
+ `dist-build/src/app/views/home/home.view.ts(9,${col}): error TS2551: Property '_svae' does not exist on type 'HomeView'. Did you mean '_save'?`,
76
+ `dist-build/src/app/views/home/home.view.ts(9,${col}): error TS2341: Property '_svae' is private and only accessible within class 'Base'.`,
77
+ ].join('\n');
78
+
79
+ expect(remapTscOutput(output, { cwd, tempDir, templateMap: MAP, handlerRefs: REFS }).split('\n')).toEqual([
80
+ "src/app/views/home/home.view.html(2,26): error TS2551: Property '_svae' does not exist on type 'HomeView'. Did you mean '_save'? (data-event handler \"_svae\" of HomeView)",
81
+ "src/app/views/home/home.view.html(2,26): error TS2341: Property '_svae' is private and only accessible within class 'Base'. (data-event handler \"_svae\" of HomeView) Declare it protected to use it in a subclass template.",
82
+ ]);
83
+ });
84
+ });
@@ -1,77 +1,43 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import path from 'path';
3
- import ts from 'typescript';
4
- import { print, errorExit } from '../../../utils/index.js';
3
+ import { errorExit } from '../../../utils/index.js';
5
4
  import { PATHS } from './paths.js';
5
+ import { builderConfig } from '../../builder.config.js';
6
+ import { findDuplicateRouteKeys, readRoutesImporters } from './route-keys.js';
6
7
 
7
- const ROUTES_FILE = path.join(PATHS.sourceApp, 'routes.ts');
8
+ const ROUTES_FILE = path.join(PATHS.sourceApp, 'routes.{{#if (eq lang "js")}}js{{else}}ts{{/if}}');
8
9
 
9
- function findAppRoutesObjectLiteral(sourceFile) {
10
- let objectLiteral;
10
+ // Only when the routes are used: imported by app code, or prerendered (generateSEOFiles bundles
11
+ // appRoutes). An app without the router (components mounted into a page) needs no routes file.
12
+ async function validateRoutes() {
13
+ const importers = readRoutesImporters(PATHS.sourceApp, ROUTES_FILE);
14
+ if (importers.length === 0 && !builderConfig.generateSEO) return;
11
15
 
12
- ts.forEachChild(sourceFile, function visit(node) {
13
- if (
14
- ts.isVariableDeclaration(node) &&
15
- ts.isIdentifier(node.name) &&
16
- node.name.text === 'appRoutes' &&
17
- node.initializer &&
18
- ts.isObjectLiteralExpression(node.initializer)
19
- ) {
20
- objectLiteral = node.initializer;
21
- return;
22
- }
23
- ts.forEachChild(node, visit);
24
- });
25
-
26
- return objectLiteral;
27
- }
28
-
29
- function getRouteKeys(objectLiteral) {
30
- return objectLiteral.properties
31
- .filter((prop) => ts.isPropertyAssignment(prop) && ts.isStringLiteralLike(prop.name))
32
- .map((prop) => prop.name.text);
33
- }
34
-
35
- function findDuplicates(keys) {
36
- const seen = new Set();
37
- const duplicates = new Set();
38
-
39
- for (const key of keys) {
40
- if (seen.has(key)) duplicates.add(key);
41
- seen.add(key);
42
- }
43
-
44
- return [...duplicates];
45
- }
46
-
47
- function validateRoutes() {
48
16
  let source;
49
17
  try {
50
18
  source = readFileSync(ROUTES_FILE, 'utf-8');
51
19
  } catch {
52
- errorExit(`Failed to read ${ROUTES_FILE}`);
20
+ const usedBy = importers.length
21
+ ? `imported by ${importers.map((file) => path.relative(process.cwd(), file)).join(', ')}`
22
+ : 'needed by generateSEOFiles';
23
+ errorExit(`Failed to read ${ROUTES_FILE} (${usedBy})`, 'validate-routes');
53
24
  }
54
25
 
55
- const sourceFile = ts.createSourceFile(ROUTES_FILE, source, ts.ScriptTarget.Latest, true);
56
- const objectLiteral = findAppRoutesObjectLiteral(sourceFile);
57
-
58
- if (!objectLiteral) {
59
- errorExit(`Could not find an "appRoutes" object literal in ${ROUTES_FILE}`);
26
+ if (!/\bappRoutes\b/.test(source)) {
27
+ errorExit(`Could not find "appRoutes" in ${ROUTES_FILE}`, 'validate-routes');
60
28
  }
61
29
 
62
- const duplicates = findDuplicates(getRouteKeys(objectLiteral));
30
+ const duplicates = await findDuplicateRouteKeys(source, { loader: '{{#if (eq lang "js")}}js{{else}}ts{{/if}}', sourcefile: ROUTES_FILE });
63
31
 
64
32
  if (duplicates.length > 0) {
65
- const list = duplicates.map((key) => `"${key}"`).join(', ');
33
+ const list = duplicates.map(({ key, line }) => `"${key}"${line ? ` (line ${line})` : ''}`).join(', ');
66
34
  errorExit(
67
- `Duplicate route key(s) in ${ROUTES_FILE}: ${list} — an earlier route with this path is silently unreachable.`
35
+ `Duplicate route key(s) in ${ROUTES_FILE}: ${list} — an earlier route with this path is silently unreachable.`,
36
+ 'validate-routes'
68
37
  );
69
38
  }
70
-
71
39
  }
72
40
 
73
- try {
74
- validateRoutes();
75
- } catch(err) {
41
+ validateRoutes().catch((err) => {
76
42
  errorExit(err, 'validate-routes');
77
- }
43
+ });
@@ -5,11 +5,17 @@ import { builderConfig } from '../builder.config.js';
5
5
  import { print, errorExit } from "../../utils/index.js";
6
6
 
7
7
  function finalizeBuild() {
8
+ removeBuildFiles();
8
9
  if (builderConfig.isProd) removeFoldersAfterBundle();
9
10
  if (builderConfig.isProd) removeNutinConfig();
10
11
  replaceDir(PATHS.temp, PATHS.build);
11
12
  }
12
13
 
14
+ // Only used during the build: compile-ts's tsconfig and merge-templates' template map.
15
+ function removeBuildFiles() {
16
+ ['tsconfig.json', '.template-map.json'].forEach((file) => fs.rmSync(path.join(PATHS.temp, file), { force: true }));
17
+ }
18
+
13
19
  function removeFoldersAfterBundle() {
14
20
  const foldersToRemove = [ 'core', 'app', path.join('..', 'config') ];
15
21
  const pathToFolder = (folder) => path.join(PATHS.tempSource, folder);
@@ -1,12 +1,9 @@
1
1
  import { writeFile } from 'fs/promises';
2
- import { builderConfig } from '../../builder.config.js';
3
2
 
4
3
  export async function addTags(htmlContent, filePath) {
5
- const scriptSrc = builderConfig.isProd ? "bundle.js" : "app/main.js";
6
-
7
4
  const tags = [
8
5
  { type: 'stylesheet', href: 'main.css', position: 'head' },
9
- { type: 'script', src: scriptSrc, position: 'body' }
6
+ { type: 'script', src: 'bundle.js', position: 'body' }
10
7
  ];
11
8
 
12
9
  let modifiedHtml = htmlContent;