@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,92 +1,62 @@
1
- import { Component, ComponentConfig, ComponentOptions } from "../../index.js";
1
+ import type { KeepableComponentConfig } from './children.helper.js';
2
2
 
3
- export interface CatalogConfig extends ComponentOptions {
4
- items: CatalogItemConfig[];
5
- elementName: string;
6
- elementTag?: keyof HTMLElementTagNameMap;
7
- selector: string;
8
- component: new (el: HTMLElement, data: any, props?: any) => Component;
9
- };
3
+ export function generateCatalog(config: Nutin.CatalogConfig, scope: Element = document.documentElement): Nutin.ComponentConfig[] {
4
+ if (!config.items || config.items.length < 1) return [];
10
5
 
11
- export interface CatalogItemBase {
12
- index: number;
13
- }
6
+ const componentConfigs: Nutin.ComponentConfig[] = [];
7
+ const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
14
8
 
15
- export type CatalogItemObject<T extends object> = T & CatalogItemBase;
9
+ containers.forEach((container) => {
10
+ if (!container || !(container instanceof HTMLElement)) return;
11
+ componentConfigs.push(...getComponentConfigItems(config, container))
12
+ })
16
13
 
17
- export interface CatalogItemPrimitive extends CatalogItemBase {
18
- value: string | number | boolean | null | undefined;
14
+ return componentConfigs;
19
15
  }
20
16
 
21
- export type CatalogItemConfig<T = any> =
22
- T extends object ? CatalogItemObject<T> : CatalogItemPrimitive;
23
-
24
- /**
25
- * ```typescript
26
- interface CatalogConfig extends ComponentOptions {
27
- items: CatalogItemConfig[];
28
- elementName: string;
29
- elementTag?: keyof HTMLElementTagNameMap;
30
- selector: string;
31
- component: new (el: HTMLElement, data: any) => Component;
32
- };
33
-
34
- type CatalogItemConfig<T = any> =
35
- T extends object ? CatalogItemObject<T> : CatalogItemPrimitive;
36
- ```
37
- */
38
- export class CatalogHelper {
39
- public static generateCatalog(config: CatalogConfig, scope: Element = document.documentElement): ComponentConfig[] {
40
- if (!config.items || config.items.length < 1) return [];
41
-
42
- const componentConfigs: ComponentConfig[] = [];
43
- const containers = scope.querySelectorAll(`[data-catalog="${config.selector}"]`);
44
-
45
- containers.forEach((container) => {
46
- if (!container || !(container instanceof HTMLElement)) return;
47
- componentConfigs.push(...this.getComponentConfigItems(config, container))
48
- })
49
-
50
- return componentConfigs;
51
- }
52
-
53
- private static getComponentConfigItems(config: CatalogConfig, container: HTMLElement): ComponentConfig[] {
54
- container.innerHTML = '';
55
- const componentConfigs: ComponentConfig[] = [];
56
- for (let i = 0; i < config.items.length; i++) {
57
- this.createElements(i, config, container);
58
- this.pushConfig(i, componentConfigs, config);
59
- }
60
- return componentConfigs;
17
+ function getComponentConfigItems(config: Nutin.CatalogConfig, container: HTMLElement): Nutin.ComponentConfig[] {
18
+ container.innerHTML = '';
19
+ const componentConfigs: Nutin.ComponentConfig[] = [];
20
+ for (let i = 0; i < config.items.length; i++) {
21
+ createElements(i, config, container);
22
+ pushConfig(i, componentConfigs, config);
61
23
  }
24
+ return componentConfigs;
25
+ }
62
26
 
63
- private static createElements(index: number, config: CatalogConfig, container: HTMLElement): void {
64
- const wrapper = document.createElement(config.elementTag || 'div');
65
- const el = document.createElement('div');
66
- wrapper.appendChild(el);
67
- el.setAttribute('data-component', `${config.elementName}-${index}`);
68
- wrapper.dataset.index = String(index);
69
- container?.appendChild(wrapper);
70
- }
27
+ function createElements(index: number, config: Nutin.CatalogConfig, container: HTMLElement): void {
28
+ const wrapper = document.createElement(config.elementTag || 'div');
29
+ const el = document.createElement('div');
30
+ wrapper.appendChild(el);
31
+ el.setAttribute('data-component', `${config.elementName}-${index}`);
32
+ wrapper.dataset.index = String(index);
33
+ container?.appendChild(wrapper);
34
+ }
71
35
 
72
- private static pushConfig(index: number, componentConfigs: ComponentConfig[], config: CatalogConfig): void {
73
- const configWithIndex = this.getConfigWithIndex(config, index)
36
+ function pushConfig(index: number, componentConfigs: Nutin.ComponentConfig[], config: Nutin.CatalogConfig): void {
37
+ const configWithIndex = getConfigWithIndex(config, index)
74
38
 
75
- const { props, defaults, normalizeKeys } = config;
76
- const options = { ...props, ...defaults, ...normalizeKeys };
39
+ const { props, defaults } = config;
40
+ const options = { ...props, ...defaults };
77
41
 
78
- componentConfigs.push(
79
- {
80
- selector: `${config.elementName}-${index}`,
81
- factory: (el) => new config.component(el, configWithIndex, options),
82
- },
83
- )
84
- }
42
+ const componentConfig: KeepableComponentConfig = {
43
+ selector: `${config.elementName}-${index}`,
44
+ factory: (el) => new config.component(el, configWithIndex, options),
45
+ };
85
46
 
86
- private static getConfigWithIndex(config: CatalogConfig, index: number): CatalogItemConfig {
47
+ if (config.keepBy) {
87
48
  const item = config.items[index];
88
- return (item && typeof item === 'object')
89
- ? { ...(item as object), index: index }
90
- : { value: item, index: index };
49
+ componentConfig.keep = config.keepBy(item, index);
50
+ componentConfig.scope = `catalog:${config.selector}:${config.elementName}`;
51
+ componentConfig.keepIf = { item, options, component: config.component };
91
52
  }
53
+
54
+ componentConfigs.push(componentConfig);
55
+ }
56
+
57
+ function getConfigWithIndex(config: Nutin.CatalogConfig, index: number): Nutin.CatalogItemConfig {
58
+ const item = config.items[index];
59
+ return (item && typeof item === 'object')
60
+ ? { ...(item as object), index: index }
61
+ : { value: item, index: index };
92
62
  }
@@ -1,4 +1,23 @@
1
- import { ChildrenHelper } from '#root/dist/src/core/base-classes/base-component/helpers/children.helper.js';
1
+ import * as ChildrenHelper from '#root/dist/src/core/base-classes/base-component/helpers/children.helper.js';
2
+ import { BaseComponent } from '#root/dist/src/core/index.js';
3
+
4
+ class Widget extends BaseComponent {
5
+ constructor(options) {
6
+ super(options);
7
+ this.renders = 0;
8
+ this.destroyed = false;
9
+ }
10
+ generateTemplate() { return '<p>widget</p>'; }
11
+ render() {
12
+ this.renders++;
13
+ return super.render();
14
+ }
15
+ destroy() {
16
+ this.destroyed = true;
17
+ this.onDestroy?.();
18
+ super.destroy();
19
+ }
20
+ }
2
21
 
3
22
  describe('ChildrenHelper', () => {
4
23
  let element;
@@ -31,13 +50,13 @@ describe('ChildrenHelper', () => {
31
50
  }],
32
51
  };
33
52
 
34
- const children = [];
35
- ChildrenHelper.addChildren(component, element, children);
53
+ const registry = ChildrenHelper.createRegistry();
54
+ ChildrenHelper.addChildren(component, element, registry);
36
55
 
37
56
  expect(factoryCalls.length).toBe(1);
38
57
  expect(factoryCalls[0]).toBe(element.querySelector('[data-component="widget"]'));
39
58
  expect(rendered.length).toBe(1);
40
- expect(children.length).toBe(1);
59
+ expect(registry.rebuilt.length).toBe(1);
41
60
  });
42
61
 
43
62
  it('instantiates one child per matched element for a single registerChildren entry', () => {
@@ -49,17 +68,41 @@ describe('ChildrenHelper', () => {
49
68
  }],
50
69
  };
51
70
 
52
- const children = [];
53
- ChildrenHelper.addChildren(component, element, children);
71
+ const registry = ChildrenHelper.createRegistry();
72
+ ChildrenHelper.addChildren(component, element, registry);
73
+
74
+ expect(registry.rebuilt.length).toBe(2);
75
+ });
76
+
77
+ it('keeps the placeholder attributes (except data-component) on the mounted child element', () => {
78
+ class Widget extends BaseComponent {
79
+ generateTemplate() { return '<p>widget</p>'; }
80
+ }
81
+ element.innerHTML = '<div data-component="widget" class="my-class" id="my-widget"></div>';
82
+ const component = {
83
+ registerChildren: () => [{
84
+ selector: 'widget',
85
+ factory: (el) => new Widget({ mountTarget: el }),
86
+ }],
87
+ };
88
+
89
+ const registry = ChildrenHelper.createRegistry();
90
+ ChildrenHelper.addChildren(component, element, registry);
54
91
 
55
- expect(children.length).toBe(2);
92
+ const childEl = registry.rebuilt[0].getElement();
93
+ expect(element.contains(childEl)).toBe(true);
94
+ expect(childEl.classList.contains('my-class')).toBe(true);
95
+ expect(childEl.id).toBe('my-widget');
96
+ expect(childEl.hasAttribute('data-component')).toBe(false);
97
+ expect(childEl.innerHTML).toBe('<p>widget</p>');
56
98
  });
57
99
 
58
100
  it('does nothing when there are no registerChildren', () => {
59
101
  const component = { registerChildren: () => [] };
60
- const children = [];
61
- ChildrenHelper.addChildren(component, element, children);
62
- expect(children.length).toBe(0);
102
+ const registry = ChildrenHelper.createRegistry();
103
+ ChildrenHelper.addChildren(component, element, registry);
104
+ expect(registry.rebuilt.length).toBe(0);
105
+ expect(registry.keepable.size).toBe(0);
63
106
  });
64
107
 
65
108
  it('destroys previously-tracked children and resets the array before mounting new ones on a second call', () => {
@@ -72,25 +115,268 @@ describe('ChildrenHelper', () => {
72
115
  }],
73
116
  };
74
117
 
75
- const children = [];
76
- ChildrenHelper.addChildren(component, element, children);
77
- expect(children.length).toBe(1);
118
+ const registry = ChildrenHelper.createRegistry();
119
+ ChildrenHelper.addChildren(component, element, registry);
120
+ expect(registry.rebuilt.length).toBe(1);
78
121
  expect(destroyCount).toBe(0);
79
122
 
80
123
  element.innerHTML = '<div data-component="widget"></div>';
81
- ChildrenHelper.addChildren(component, element, children);
124
+ ChildrenHelper.addChildren(component, element, registry);
82
125
 
83
126
  expect(destroyCount).toBe(1);
84
- expect(children.length).toBe(1);
127
+ expect(registry.rebuilt.length).toBe(1);
85
128
  });
86
129
 
87
- it('destroyChildren calls destroy on every child', () => {
130
+ it('destroyChildren calls destroy on every keepable and rebuilt child and empties the registry', () => {
88
131
  let destroyCount = 0;
89
- const children = [
90
- { destroy: () => destroyCount++ },
91
- { destroy: () => destroyCount++ },
92
- ];
93
- ChildrenHelper.destroyChildren(children);
132
+ const registry = ChildrenHelper.createRegistry();
133
+ registry.rebuilt.push({ destroy: () => destroyCount++ });
134
+ registry.keepable.set('a', { instance: { destroy: () => destroyCount++ } });
135
+ ChildrenHelper.destroyChildren(registry);
94
136
  expect(destroyCount).toBe(2);
137
+ expect(registry.rebuilt.length).toBe(0);
138
+ expect(registry.keepable.size).toBe(0);
139
+ });
140
+
141
+ it('child with an unchanged keep value is moved into the new placeholder without calling its factory or rendering it', () => {
142
+ const created = [];
143
+ const component = {
144
+ registerChildren: () => [{
145
+ selector: 'widget',
146
+ keep: 'w',
147
+ factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
148
+ }],
149
+ };
150
+ const registry = ChildrenHelper.createRegistry();
151
+
152
+ element.innerHTML = '<div data-component="widget"></div>';
153
+ ChildrenHelper.addChildren(component, element, registry);
154
+ element.innerHTML = '<div data-component="widget"></div>';
155
+ ChildrenHelper.addChildren(component, element, registry);
156
+
157
+ expect(created.length).toBe(1);
158
+ expect(created[0].renders).toBe(1);
159
+ expect(created[0].destroyed).toBe(false);
160
+ expect(element.contains(created[0].getElement())).toBe(true);
161
+ expect(element.querySelector('[data-component="widget"]')).toBe(null);
162
+ expect(registry.kept).toEqual([created[0]]);
163
+ });
164
+
165
+ it('child whose keep value changed is destroyed and recreated', () => {
166
+ let keep = 1;
167
+ const created = [];
168
+ const component = {
169
+ registerChildren: () => [{
170
+ selector: 'widget',
171
+ keep,
172
+ factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
173
+ }],
174
+ };
175
+ const registry = ChildrenHelper.createRegistry();
176
+
177
+ element.innerHTML = '<div data-component="widget"></div>';
178
+ ChildrenHelper.addChildren(component, element, registry);
179
+ keep = 2;
180
+ element.innerHTML = '<div data-component="widget"></div>';
181
+ ChildrenHelper.addChildren(component, element, registry);
182
+
183
+ expect(created.length).toBe(2);
184
+ expect(created[0].destroyed).toBe(true);
185
+ expect(created[1].destroyed).toBe(false);
186
+ expect(registry.keepable.size).toBe(1);
187
+ expect(registry.kept.length).toBe(0);
188
+ });
189
+
190
+ it('keepable child whose placeholder disappeared is destroyed', () => {
191
+ const created = [];
192
+ const component = {
193
+ registerChildren: () => [{
194
+ selector: 'widget',
195
+ keep: 'w',
196
+ factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
197
+ }],
198
+ };
199
+ const registry = ChildrenHelper.createRegistry();
200
+
201
+ element.innerHTML = '<div data-component="widget"></div>';
202
+ ChildrenHelper.addChildren(component, element, registry);
203
+ element.innerHTML = '';
204
+ ChildrenHelper.addChildren(component, element, registry);
205
+
206
+ expect(created[0].destroyed).toBe(true);
207
+ expect(registry.keepable.size).toBe(0);
208
+ });
209
+
210
+ it('destroys removed children before creating new ones', () => {
211
+ const order = [];
212
+ let keep = 1;
213
+ const component = {
214
+ registerChildren: () => [{
215
+ selector: 'widget',
216
+ keep,
217
+ factory: (el) => {
218
+ const w = new Widget({ mountTarget: el });
219
+ const id = keep;
220
+ order.push(`create ${id}`);
221
+ w.onDestroy = () => order.push(`destroy ${id}`);
222
+ return w;
223
+ },
224
+ }],
225
+ };
226
+ const registry = ChildrenHelper.createRegistry();
227
+
228
+ element.innerHTML = '<div data-component="widget"></div>';
229
+ ChildrenHelper.addChildren(component, element, registry);
230
+ keep = 2;
231
+ element.innerHTML = '<div data-component="widget"></div>';
232
+ ChildrenHelper.addChildren(component, element, registry);
233
+
234
+ expect(order).toEqual(['create 1', 'destroy 1', 'create 2']);
235
+ });
236
+
237
+ it('the same keep value under two placeholders of one selector gives each its own identity', () => {
238
+ const created = [];
239
+ const component = {
240
+ registerChildren: () => [{
241
+ selector: 'widget',
242
+ keep: 'w',
243
+ factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
244
+ }],
245
+ };
246
+ const registry = ChildrenHelper.createRegistry();
247
+
248
+ element.innerHTML = '<div data-component="widget"></div><div data-component="widget"></div>';
249
+ ChildrenHelper.addChildren(component, element, registry);
250
+ element.innerHTML = '<div data-component="widget"></div><div data-component="widget"></div>';
251
+ ChildrenHelper.addChildren(component, element, registry);
252
+
253
+ expect(created.length).toBe(2);
254
+ expect(registry.kept.length).toBe(2);
255
+ });
256
+
257
+ it('warns about a duplicate keep value and recreates the duplicate on every render', () => {
258
+ const warnSpy = spyOn(console, 'warn').andCallFake(() => {});
259
+ const component = {
260
+ registerChildren: () => [
261
+ { selector: 'widget', keep: 'w', factory: (el) => new Widget({ mountTarget: el }) },
262
+ { selector: 'widget', keep: 'w', factory: (el) => new Widget({ mountTarget: el }) },
263
+ ],
264
+ };
265
+ const registry = ChildrenHelper.createRegistry();
266
+
267
+ element.innerHTML = '<div data-component="widget"></div>';
268
+ ChildrenHelper.addChildren(component, element, registry);
269
+ warnSpy.restore();
270
+
271
+ expect(warnSpy.callCount).toBe(1);
272
+ expect(registry.keepable.size).toBe(1);
273
+ expect(registry.rebuilt.length).toBe(1);
274
+ });
275
+
276
+ it('keptCount a catalog-style child only when its keepIf item, options and component are shallow-equal', () => {
277
+ const created = [];
278
+ let item = { id: 1, name: 'a' };
279
+ let options = { tone: 'x' };
280
+ const component = {
281
+ registerChildren: () => [{
282
+ selector: 'item-0',
283
+ keep: item.id,
284
+ scope: 'catalog:list:item',
285
+ keepIf: { item, options, component: Widget },
286
+ factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
287
+ }],
288
+ };
289
+ const registry = ChildrenHelper.createRegistry();
290
+ const rerender = () => {
291
+ element.innerHTML = '<div data-component="item-0"></div>';
292
+ ChildrenHelper.addChildren(component, element, registry);
293
+ };
294
+
295
+ rerender();
296
+ item = { id: 1, name: 'a' };
297
+ options = { tone: 'x' };
298
+ rerender();
299
+ expect(created.length).toBe(1);
300
+
301
+ item = { id: 1, name: 'b' };
302
+ rerender();
303
+ expect(created.length).toBe(2);
304
+
305
+ options = { tone: 'y' };
306
+ rerender();
307
+ expect(created.length).toBe(3);
308
+ });
309
+
310
+ it('recreates a catalog-style child when its keepIf item changes between null, an object and an array', () => {
311
+ const created = [];
312
+ let item = null;
313
+ const component = {
314
+ registerChildren: () => [{
315
+ selector: 'item-0',
316
+ keep: 1,
317
+ scope: 'catalog:list:item',
318
+ keepIf: { item, options: undefined, component: Widget },
319
+ factory: (el) => { const w = new Widget({ mountTarget: el }); created.push(w); return w; },
320
+ }],
321
+ };
322
+ const registry = ChildrenHelper.createRegistry();
323
+ const rerender = () => {
324
+ element.innerHTML = '<div data-component="item-0"></div>';
325
+ ChildrenHelper.addChildren(component, element, registry);
326
+ };
327
+
328
+ rerender();
329
+ item = { 0: 'a' };
330
+ rerender();
331
+ expect(created.length).toBe(2);
332
+
333
+ item = ['a'];
334
+ rerender();
335
+ expect(created.length).toBe(3);
336
+ });
337
+
338
+ it('ignores a non-HTML placeholder element', () => {
339
+ let calls = 0;
340
+ const component = {
341
+ registerChildren: () => [{
342
+ selector: 'icon',
343
+ factory: (el) => { calls++; return new Widget({ mountTarget: el }); },
344
+ }],
345
+ };
346
+ element.innerHTML = '<svg data-component="icon"></svg>';
347
+
348
+ ChildrenHelper.addChildren(component, element, ChildrenHelper.createRegistry());
349
+
350
+ expect(calls).toBe(0);
351
+ });
352
+
353
+ it('moves a kept child with Element.moveBefore when the browser supports it', () => {
354
+ const moved = [];
355
+ const proto = Element.prototype;
356
+ proto.moveBefore = function (node, child) {
357
+ moved.push(node);
358
+ this.insertBefore(node, child);
359
+ };
360
+
361
+ try {
362
+ const component = {
363
+ registerChildren: () => [{ selector: 'widget', keep: 'w', factory: (el) => new Widget({ mountTarget: el }) }],
364
+ };
365
+ const registry = ChildrenHelper.createRegistry();
366
+ element.innerHTML = '<div data-component="widget"></div>';
367
+ ChildrenHelper.addChildren(component, element, registry);
368
+ const [{ instance }] = registry.keepable.values();
369
+
370
+ // Stand-in for a parked child: still connected, outside the new placeholder.
371
+ document.body.appendChild(instance.getElement());
372
+ element.innerHTML = '<div data-component="widget"></div>';
373
+ ChildrenHelper.addChildren(component, element, registry);
374
+
375
+ expect(moved).toEqual([instance.getElement()]);
376
+ expect(element.contains(instance.getElement())).toBe(true);
377
+ expect(element.querySelector('[data-component="widget"]')).toBe(null);
378
+ } finally {
379
+ delete proto.moveBefore;
380
+ }
95
381
  });
96
382
  });
@@ -1,31 +1,149 @@
1
- import { BaseComponent, ComponentConfig } from '../base-component.js';
2
-
3
- export class ChildrenHelper {
4
- public static addChildren(
5
- component: BaseComponent,
6
- element: HTMLElement,
7
- children: BaseComponent[]
8
- ): void {
9
- this.destroyChildren(children);
10
- children.length = 0;
11
-
12
- const configs: ComponentConfig[] = component.registerChildren();
13
- configs.forEach(config => {
14
- element.querySelectorAll(`[data-component="${config.selector}"]`).forEach(el => {
15
- if (el instanceof HTMLElement) {
16
- const childComponent = config.factory(el);
17
- childComponent.render();
18
- this.registerChild(childComponent, children);
19
- }
20
- });
1
+ import type { BaseComponent } from '../base-component.js';
2
+ import * as DomHelper from './dom.helper.js';
3
+
4
+ // What a catalog item was built from: a kept item must still match it.
5
+ export interface KeepCondition {
6
+ item: unknown;
7
+ options: Record<string, unknown>;
8
+ component: unknown;
9
+ }
10
+
11
+ // Internal fields CatalogHelper sets on top of the public ComponentConfig.
12
+ export interface KeepableComponentConfig extends Nutin.ComponentConfig {
13
+ scope?: string;
14
+ keepIf?: KeepCondition;
15
+ }
16
+
17
+ interface KeepableChild {
18
+ instance: BaseComponent;
19
+ keepIf?: KeepCondition;
20
+ }
21
+
22
+ export interface ChildRegistry {
23
+ keepable: Map<string, KeepableChild>;
24
+ rebuilt: BaseComponent[];
25
+ kept: BaseComponent[];
26
+ }
27
+
28
+ interface Placement {
29
+ config: KeepableComponentConfig;
30
+ placeholder: HTMLElement;
31
+ id?: string;
32
+ kept?: KeepableChild;
33
+ }
34
+
35
+ export function createRegistry(): ChildRegistry {
36
+ return { keepable: new Map(), rebuilt: [], kept: [] };
37
+ }
38
+
39
+ // Children whose keep value is unchanged are moved into their new
40
+ // placeholder as-is; everything else is destroyed and recreated.
41
+ export function addChildren(
42
+ component: BaseComponent,
43
+ element: HTMLElement,
44
+ registry: ChildRegistry
45
+ ): void {
46
+ const placements = matchPlaceholders(component.registerChildren(), element, registry);
47
+ destroyUnkept(registry, placements);
48
+
49
+ // Every kept child moves before any new one renders, so move targets are
50
+ // prepared once rather than after each DOM change.
51
+ const kept = placements.filter(({ kept }) => kept);
52
+ kept.forEach(({ kept, placeholder }) => DomHelper.prepareMoveTarget(placeholder.parentNode, kept!.instance.getElement()));
53
+ kept.forEach(placement => keep(placement, registry));
54
+ placements.filter(({ kept }) => !kept).forEach(placement => create(placement, registry));
55
+ }
56
+
57
+ export function getKeepableElements(registry: ChildRegistry): HTMLElement[] {
58
+ return Array.from(registry.keepable.values(), ({ instance }) => instance.getElement());
59
+ }
60
+
61
+ export function destroyChildren(registry: ChildRegistry): void {
62
+ registry.rebuilt.forEach(child => child.destroy());
63
+ registry.keepable.forEach(({ instance }) => instance.destroy());
64
+ registry.rebuilt = [];
65
+ registry.keepable.clear();
66
+ registry.kept = [];
67
+ }
68
+
69
+ function matchPlaceholders(
70
+ configs: KeepableComponentConfig[],
71
+ element: HTMLElement,
72
+ registry: ChildRegistry
73
+ ): Placement[] {
74
+ const placements: Placement[] = [];
75
+ const seen = new Set<string>();
76
+
77
+ configs.forEach(config => {
78
+ element.querySelectorAll(`[data-component="${config.selector}"]`).forEach((placeholder, occurrence) => {
79
+ if (!(placeholder instanceof HTMLElement)) return;
80
+ const id = getId(config, occurrence, seen);
81
+ const previous = id === undefined ? undefined : registry.keepable.get(id);
82
+ const kept = previous && canKeep(previous, config) ? previous : undefined;
83
+ placements.push({ config, placeholder, id, kept });
21
84
  });
22
- }
85
+ });
23
86
 
24
- public static destroyChildren(children: BaseComponent[]): void {
25
- children.forEach(child => child.destroy());
26
- }
87
+ return placements;
88
+ }
27
89
 
28
- private static registerChild(child: BaseComponent, children: BaseComponent[]): void {
29
- children.push(child);
90
+ function getId(config: KeepableComponentConfig, occurrence: number, seen: Set<string>): string | undefined {
91
+ if (config.keep === undefined) return undefined;
92
+
93
+ const scope = config.scope ?? config.selector;
94
+ const id = JSON.stringify([scope, config.keep, occurrence]);
95
+ if (seen.has(id)) {
96
+ console.warn(`Duplicate child keep value "${config.keep}" in "${scope}" - it will be recreated on every render.`);
97
+ return undefined;
30
98
  }
99
+ seen.add(id);
100
+ return id;
101
+ }
102
+
103
+ function canKeep(previous: KeepableChild, config: KeepableComponentConfig): boolean {
104
+ const before = previous.keepIf;
105
+ const after = config.keepIf;
106
+ if (!before || !after) return before === after;
107
+
108
+ return before.component === after.component
109
+ && shallowEqual(before.item, after.item)
110
+ && shallowEqual(before.options, after.options);
111
+ }
112
+
113
+ function destroyUnkept(registry: ChildRegistry, placements: Placement[]): void {
114
+ const keptIds = new Set(placements.filter(({ kept }) => kept).map(({ id }) => id));
115
+
116
+ registry.rebuilt.forEach(child => child.destroy());
117
+ registry.rebuilt = [];
118
+ registry.kept = [];
119
+ registry.keepable.forEach((entry, id) => {
120
+ if (keptIds.has(id)) return;
121
+ entry.instance.destroy();
122
+ registry.keepable.delete(id);
123
+ });
124
+ }
125
+
126
+ function keep({ config, placeholder, kept }: Placement, registry: ChildRegistry): void {
127
+ DomHelper.replacePlaceholder(kept!.instance.getElement(), placeholder);
128
+ kept!.keepIf = config.keepIf;
129
+ registry.kept.push(kept!.instance);
130
+ }
131
+
132
+ function create({ config, placeholder, id }: Placement, registry: ChildRegistry): void {
133
+ const child = config.factory(placeholder);
134
+ child.render();
135
+ if (id === undefined) registry.rebuilt.push(child);
136
+ else registry.keepable.set(id, { instance: child, keepIf: config.keepIf });
137
+ }
138
+
139
+ function shallowEqual(a: unknown, b: unknown): boolean {
140
+ if (Object.is(a, b)) return true;
141
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
142
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
143
+
144
+ const aRecord = a as Record<string, unknown>;
145
+ const bRecord = b as Record<string, unknown>;
146
+ const aKeys = Object.keys(aRecord);
147
+ return aKeys.length === Object.keys(bRecord).length
148
+ && aKeys.every(key => Object.prototype.hasOwnProperty.call(bRecord, key) && Object.is(aRecord[key], bRecord[key]));
31
149
  }