cabloy 5.1.108 → 5.1.110

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 (192) hide show
  1. package/.cabloy-version +1 -1
  2. package/.gitignore +5 -2
  3. package/CHANGELOG.md +43 -0
  4. package/cabloy-docs/.vitepress/config.mjs +1 -0
  5. package/cabloy-docs/ai/playbook-module-removal.md +1 -1
  6. package/cabloy-docs/ai/virtual-decorator-guidance.md +9 -9
  7. package/cabloy-docs/backend/websocket-call-flow.md +1 -1
  8. package/cabloy-docs/backend/websocket-guide.md +1 -1
  9. package/cabloy-docs/frontend/fetch-interceptor-guide.md +8 -8
  10. package/cabloy-docs/frontend/form-guide.md +3 -1
  11. package/cabloy-docs/frontend/form-layout-guide.md +322 -0
  12. package/cabloy-docs/frontend/introduction.md +1 -0
  13. package/cabloy-docs/frontend/resource-entry-page-deep-dive.md +2 -0
  14. package/cabloy-docs/frontend/router-tabs-layout-integration.md +2 -1
  15. package/cabloy-docs/frontend/scripts.md +56 -1
  16. package/cabloy-docs/frontend/ssr-build-deploy-guide.md +1 -1
  17. package/cabloy-docs/frontend/ssr-overview.md +6 -0
  18. package/cabloy-docs/frontend/ssr-review-checklist.md +1 -1
  19. package/cabloy-docs/frontend/ssr-troubleshooting-guide.md +9 -9
  20. package/cabloy-docs/frontend/table-resource-crud-cookbook.md +33 -1
  21. package/cabloy-docs/frontend/zova-form-source-reading-map.md +36 -4
  22. package/cabloy-docs/frontend/zova-form-under-the-hood.md +13 -0
  23. package/cabloy-docs/fullstack/framework-performance.md +3 -3
  24. package/cabloy-docs/fullstack/quickstart.md +14 -0
  25. package/cabloy-docs/reference/repo-scripts.md +79 -0
  26. package/e2e/config/playwright.basic.config.ts +3 -0
  27. package/e2e/config/playwright.commerce.config.ts +3 -0
  28. package/e2e/config/playwright.shared.config.ts +38 -0
  29. package/e2e/scripts/e2e.ts +33 -0
  30. package/e2e/scripts/startE2eVona.ts +38 -0
  31. package/e2e/scripts/testE2eClean.ts +56 -0
  32. package/e2e/specs/a-basic/basic.spec.ts +271 -0
  33. package/e2e/specs/a-commerce/commerce.spec.ts +61 -0
  34. package/package.json +17 -1
  35. package/scripts/upgrade.ts +182 -7
  36. package/test-results/.last-run.json +4 -0
  37. package/vona/packages-vona/vona/package.json +1 -1
  38. package/vona/pnpm-lock.yaml +304 -45
  39. package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +52 -0
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +29 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/this.ts +2 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/index.ts +1 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/tsconfig.build.json +11 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/tsconfig.json +7 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-member/package.json +52 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +29 -0
  47. package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/this.ts +2 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-member/src/index.ts +1 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-member/tsconfig.build.json +11 -0
  50. package/vona/src/suite/a-commerce/modules/commerce-member/tsconfig.json +7 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +52 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +29 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/this.ts +2 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-payment/src/index.ts +1 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-payment/tsconfig.build.json +11 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-payment/tsconfig.json +7 -0
  57. package/vona/src/suite/a-commerce/modules/commerce-promotion/package.json +52 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +29 -0
  59. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/this.ts +2 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
  61. package/vona/src/suite/a-commerce/modules/commerce-promotion/tsconfig.build.json +11 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-promotion/tsconfig.json +7 -0
  63. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/package.json +53 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +81 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/this.ts +2 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/ssrMenu.home.ts +21 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/bean/ssrSite.commerceAdmin.ts +34 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/index.ts +1 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/tsconfig.build.json +11 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/tsconfig.json +7 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-siteweb/package.json +53 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +81 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/this.ts +2 -0
  74. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +20 -0
  75. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrSite.commerce.ts +34 -0
  76. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
  77. package/vona/src/suite/a-commerce/modules/commerce-siteweb/tsconfig.build.json +11 -0
  78. package/vona/src/suite/a-commerce/modules/commerce-siteweb/tsconfig.json +7 -0
  79. package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +52 -0
  80. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +29 -0
  81. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/this.ts +2 -0
  82. package/vona/src/suite/a-commerce/modules/commerce-trade/src/index.ts +1 -0
  83. package/vona/src/suite/a-commerce/modules/commerce-trade/tsconfig.build.json +11 -0
  84. package/vona/src/suite/a-commerce/modules/commerce-trade/tsconfig.json +7 -0
  85. package/vona/src/suite/a-commerce/package.json +18 -0
  86. package/vona/src/suite/a-commerce/tsconfig.base.json +4 -0
  87. package/vona/src/suite/a-commerce/tsconfig.json +28 -0
  88. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +2 -0
  89. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +2 -0
  90. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +50 -1
  91. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +21 -1
  92. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +47 -1
  93. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +47 -1
  94. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +61 -2
  95. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  96. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/bean/bean.database.ts +1 -1
  97. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/bean/schedule.softDeletionPrune.ts +1 -1
  98. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_meta.ts +1 -1
  99. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAggregate.ts +1 -1
  100. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoCreate.ts +1 -1
  101. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGroup.ts +1 -1
  102. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoSelectAndCount.ts +1 -1
  103. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoUpdate.ts +1 -1
  104. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/modelCacheBase.ts +1 -1
  105. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/relations.ts +1 -1
  106. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/relationsDynamic.ts +1 -1
  107. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/relationsMutate.ts +1 -1
  108. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/relationsStatic.ts +1 -1
  109. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/database.ts +1 -1
  110. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/databaseDialectBase_.ts +10 -3
  111. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/db_.ts +1 -1
  112. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  113. package/zova/env/.env.cabloyCommerce +24 -0
  114. package/zova/env/.env.cabloyCommerceAdmin +19 -0
  115. package/zova/package.original.json +10 -1
  116. package/zova/packages-zova/zova/package.json +2 -2
  117. package/zova/pnpm-lock.yaml +145 -31
  118. package/zova/src/front/config/config/config.cabloyCommerce.ts +16 -0
  119. package/zova/src/front/config/config/config.cabloyCommerceAdmin.ts +13 -0
  120. package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +52 -0
  121. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +26 -0
  122. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/this.ts +2 -0
  123. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/index.ts +1 -0
  124. package/zova/src/suite/a-commerce/modules/commerce-catalog/tsconfig.build.json +13 -0
  125. package/zova/src/suite/a-commerce/modules/commerce-catalog/tsconfig.json +5 -0
  126. package/zova/src/suite/a-commerce/modules/commerce-member/package.json +52 -0
  127. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +26 -0
  128. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/this.ts +2 -0
  129. package/zova/src/suite/a-commerce/modules/commerce-member/src/index.ts +1 -0
  130. package/zova/src/suite/a-commerce/modules/commerce-member/tsconfig.build.json +13 -0
  131. package/zova/src/suite/a-commerce/modules/commerce-member/tsconfig.json +5 -0
  132. package/zova/src/suite/a-commerce/modules/commerce-payment/package.json +52 -0
  133. package/zova/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +26 -0
  134. package/zova/src/suite/a-commerce/modules/commerce-payment/src/.metadata/this.ts +2 -0
  135. package/zova/src/suite/a-commerce/modules/commerce-payment/src/index.ts +1 -0
  136. package/zova/src/suite/a-commerce/modules/commerce-payment/tsconfig.build.json +13 -0
  137. package/zova/src/suite/a-commerce/modules/commerce-payment/tsconfig.json +5 -0
  138. package/zova/src/suite/a-commerce/modules/commerce-promotion/package.json +52 -0
  139. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +26 -0
  140. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/this.ts +2 -0
  141. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
  142. package/zova/src/suite/a-commerce/modules/commerce-promotion/tsconfig.build.json +13 -0
  143. package/zova/src/suite/a-commerce/modules/commerce-promotion/tsconfig.json +5 -0
  144. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/package.json +52 -0
  145. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/index.ts +26 -0
  146. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/.metadata/this.ts +2 -0
  147. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/index.ts +1 -0
  148. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/tsconfig.build.json +13 -0
  149. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/tsconfig.json +5 -0
  150. package/zova/src/suite/a-commerce/modules/commerce-siteweb/package.json +52 -0
  151. package/zova/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +26 -0
  152. package/zova/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/this.ts +2 -0
  153. package/zova/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
  154. package/zova/src/suite/a-commerce/modules/commerce-siteweb/tsconfig.build.json +13 -0
  155. package/zova/src/suite/a-commerce/modules/commerce-siteweb/tsconfig.json +5 -0
  156. package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +52 -0
  157. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +26 -0
  158. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/this.ts +2 -0
  159. package/zova/src/suite/a-commerce/modules/commerce-trade/src/index.ts +1 -0
  160. package/zova/src/suite/a-commerce/modules/commerce-trade/tsconfig.build.json +13 -0
  161. package/zova/src/suite/a-commerce/modules/commerce-trade/tsconfig.json +5 -0
  162. package/zova/src/suite/a-commerce/package.json +18 -0
  163. package/zova/src/suite/a-commerce/tsconfig.base.json +4 -0
  164. package/zova/src/suite/a-commerce/tsconfig.json +4 -0
  165. package/zova/src/suite/cabloy-basic/modules/basic-form/src/.metadata/component/blockFormLayout.ts +31 -0
  166. package/zova/src/suite/cabloy-basic/modules/basic-form/src/.metadata/index.ts +13 -0
  167. package/zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx +188 -0
  168. package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/component/blockFilterActions.ts +31 -0
  169. package/zova/src/suite/cabloy-basic/modules/basic-page/src/.metadata/index.ts +13 -0
  170. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockFilter/controller.tsx +61 -31
  171. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockFilterActions/controller.tsx +51 -0
  172. package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +10 -0
  173. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +9 -2
  174. package/zova/src/suite-vendor/a-zova/modules/a-app/package.json +1 -1
  175. package/zova/src/suite-vendor/a-zova/modules/a-app/src/component/app/controller.tsx +6 -0
  176. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +5 -3
  177. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +44 -3
  178. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/render.tsx +22 -4
  179. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +3 -2
  180. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts +203 -0
  181. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/index.ts +1 -0
  182. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formField.ts +7 -4
  183. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formLayout.ts +63 -0
  184. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/index.ts +1 -0
  185. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  186. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +1 -1
  187. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formLayout.ts +56 -0
  188. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/index.ts +1 -0
  189. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  190. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +19 -1
  191. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +5 -0
  192. package/zova/src/suite-vendor/a-zova/package.json +5 -5
@@ -162,8 +162,8 @@ This block uses `ZForm` with:
162
162
 
163
163
  - `schema={$$page.schemaFilter}`
164
164
  - `schemaScene="filter"`
165
- - inline layout
166
165
  - page-owned filter data
166
+ - field-level `formFieldLayout.inline: true` by default
167
167
 
168
168
  Its job is not to duplicate the list query logic.
169
169
 
@@ -178,6 +178,38 @@ A practical rule is:
178
178
  - if you need to refine which filter fields exist, start from backend filter-side metadata and schema
179
179
  - if you need to refine how filter submission affects the list, inspect `blockFilter` and `blockPage.onFilter(...)`
180
180
 
181
+ ### Use blocks for a structural filter layout
182
+
183
+ A bare `basic-page:blockFilter` keeps the default schema field rendering and adds Search/Reset controls automatically. For a structured filter, compose the existing form layout block with the filter-specific action block:
184
+
185
+ ```tsx
186
+ ZovaRender.block('basic-page:blockFilter', {
187
+ formFieldLayout: { inline: true },
188
+ blocks: [
189
+ ZovaRender.block('basic-form:blockFormLayout', {
190
+ formLayout: {
191
+ children: [
192
+ {
193
+ type: 'section',
194
+ layout: 'flow',
195
+ children: [
196
+ { type: 'field', name: 'name' },
197
+ { type: 'field', name: 'level' },
198
+ { type: 'field', name: 'createdAt' },
199
+ ],
200
+ },
201
+ ],
202
+ },
203
+ }),
204
+ ZovaRender.block('basic-page:blockFilterActions'),
205
+ ],
206
+ });
207
+ ```
208
+
209
+ `basic-form:blockFormLayout` only places schema fields. `basic-page:blockFilterActions` owns Search/Reset placement and invokes the filter command surface supplied through the form scope, so it preserves filter normalization and page-query behavior. A nonempty `blocks` list replaces the automatic body and footer; include the action block explicitly to make the filter operable.
210
+
211
+ `ZForm.inline` is no longer a form API. Use `formFieldLayout.inline` for field-level compact layout, or use blocks plus `basic-form:blockFormLayout` for structural layout. The flow section above keeps compact filters left-packed and wrapping; use the default Grid section when fields need responsive columns and spans. Read [Form Layout Guide](/frontend/form-layout-guide) for the full node grammar, section layout rules, resolver behavior, tabs, and entry-form composition.
212
+
181
213
  ## Step 5: Let `blockToolbarBulk` own bulk-action display
182
214
 
183
215
  The standard bulk toolbar block is:
@@ -218,7 +218,39 @@ Use this path when you are asking questions like:
218
218
  - `home-login/render.tsx` shows a page-level `formProvider` override for layout behavior
219
219
  - `formField/render.tsx` shows how the behavior-wrapped render path leads to the final vnode
220
220
 
221
- ## 7. Resource-driven CRUD page integration
221
+ ## 7. DTO-driven structural Form Layout
222
+
223
+ Use this path when you are asking questions like:
224
+
225
+ - where does `formLayout` come from in a resource DTO?
226
+ - how are fields, sections, groups, and tabs normalized before rendering?
227
+ - why are omitted visible fields appended or duplicate fields removed?
228
+ - where does Cabloy Basic render responsive grids and tab error badges?
229
+
230
+ ### Read the docs first
231
+
232
+ - [Form Layout Guide](/frontend/form-layout-guide)
233
+ - [Table + Resource CRUD Cookbook](/frontend/table-resource-crud-cookbook)
234
+ - [Resource Entry Page Deep Dive](/frontend/resource-entry-page-deep-dive)
235
+
236
+ ### Then read source in this order
237
+
238
+ 1. `vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx`
239
+ 2. `vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx`
240
+ 3. `zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formLayout.ts`
241
+ 4. `zova/src/suite-vendor/a-zova/modules/a-form/src/lib/formLayout.ts`
242
+ 5. `zova/src/suite/cabloy-basic/modules/basic-form/src/component/blockFormLayout/controller.tsx`
243
+ 6. `vona/src/suite/a-training/modules/training-student/test/student.test.ts`
244
+
245
+ ### What each file clarifies
246
+
247
+ - the Student DTOs show the entry and filter block composition that supplies layout metadata
248
+ - the OpenAPI type contract defines the legal node grammar and responsive values
249
+ - the resolver reconciles metadata with visible schema fields, generated IDs, and diagnostics
250
+ - the Basic block controller renders sections, groups, tabs, and field spans while delegating widgets to `$$form.renderField(...)`
251
+ - the Student test verifies emitted metadata nesting, columns, spans, and optional IDs; it is not a browser rendering test
252
+
253
+ ## 8. Resource-driven CRUD page integration
222
254
 
223
255
  Use this path when you are asking questions like:
224
256
 
@@ -251,7 +283,7 @@ If your next question becomes how `formScene` becomes `formMeta`, then `pageMeta
251
283
 
252
284
  If your next question becomes how backend/entity schema metadata attaches field-side effects through `ZovaRender.onEffect(...)` and command chains, continue with [Schema-Driven Field Effects Guide](/frontend/schema-driven-field-effects-guide).
253
285
 
254
- ## 8. Representative specimens to read before editing the framework
286
+ ## 9. Representative specimens to read before editing the framework
255
287
 
256
288
  Use this section when you want one small example before reading the framework internals.
257
289
 
@@ -267,7 +299,7 @@ Use this section when you want one small example before reading the framework in
267
299
  - `home-login` shows a real page using presets, blank rows, and provider-level layout override
268
300
  - together they give you the public authoring shape before you descend into the form runtime
269
301
 
270
- ## 9. A compact reading strategy
302
+ ## 10. A compact reading strategy
271
303
 
272
304
  When in doubt, use this order:
273
305
 
@@ -280,7 +312,7 @@ When in doubt, use this order:
280
312
 
281
313
  That order usually gets you to the answer faster than starting from the deepest runtime files first.
282
314
 
283
- ## 10. Final takeaway
315
+ ## 11. Final takeaway
284
316
 
285
317
  The fastest way to read Zova Form accurately is not to memorize every file in `a-form`.
286
318
 
@@ -410,6 +410,19 @@ zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/render.tsx
410
410
 
411
411
  That means automatic schema-driven rendering is not happening magically in the wrapper component. It is happening in the render bean.
412
412
 
413
+ ### Structural Form Layout boundary
414
+
415
+ When `ZForm` receives a nonempty block list, the render bean delegates body rendering to those blocks instead of iterating schema fields directly. For Cabloy Basic structural forms, `basic-form:blockFormLayout` resolves `formLayout` against the form's current schema properties and calls `$$form.renderField(...)` for each surviving layout field.
416
+
417
+ This keeps ownership separate:
418
+
419
+ - the form controller owns schema properties, field state, validation, and field rendering
420
+ - the shared form-layout resolver normalizes field placement metadata
421
+ - the Basic layout block renders sections, groups, grids, and tabs
422
+ - field-layout behaviors still own each field wrapper and its visible validation message
423
+
424
+ Read [Form Layout Guide](/frontend/form-layout-guide) for the DTO authoring grammar, resolver behavior, and Basic-specific responsive/tab behavior.
425
+
413
426
  ### Field render path
414
427
 
415
428
  `RenderFormField` does two important jobs:
@@ -42,17 +42,17 @@ For the current public explanation of this backend capability, see [Cache Guide]
42
42
 
43
43
  This is the kind of result Cabloy is designed to support: a framework that stays operationally calm even when the system keeps running for long periods.
44
44
 
45
- One internally generated project was kept running continuously for **21 days**. At one representative PM2 snapshot, the process looked like this:
45
+ One internally generated project was kept running continuously for **35 days**. At one representative PM2 snapshot, the process looked like this:
46
46
 
47
47
  ```text
48
48
  ┌────┬─────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
49
49
  │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
50
50
  ├────┼─────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
51
- │ 0 │ cabloy_*** │ default │ N/A │ cluster │ 226947 │ 21D │ 17 │ online │ 0% │ 435.0mb │ ubuntu │ disabled │
51
+ │ 0 │ cabloy_*** │ default │ N/A │ cluster │ 226947 │ 21D │ 17 │ online │ 0% │ 427.8mb │ ubuntu │ disabled │
52
52
  └────┴─────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
53
53
  ```
54
54
 
55
- For this 21-day continuous run, the observed result was **0 memory leak**.
55
+ For this 35-day continuous run, the observed result was **0 memory leak**.
56
56
 
57
57
  The value of this example is not that it is a synthetic micro-benchmark. The value is that it reflects a real long-running project process with a clear, inspectable runtime footprint.
58
58
 
@@ -102,10 +102,24 @@ These commands build the edition-specific frontend flavors from the repository y
102
102
 
103
103
  ## 6. Upgrade an existing project
104
104
 
105
+ Inspect the planned framework changes before applying them:
106
+
105
107
  ```bash
108
+ npm run upgrade:dry-run
106
109
  npm run upgrade
107
110
  ```
108
111
 
112
+ In Cabloy Basic, upgrade synchronizes the framework-owned SSR browser E2E baseline, its suite/surface root `test:e2e:*` commands, and the `@playwright/test` development dependency. It does not add a root script for each individual E2E scenario. The framework reserves these paths:
113
+
114
+ ```text
115
+ e2e/config/
116
+ e2e/scripts/
117
+ e2e/specs/a-basic/
118
+ e2e/specs/a-commerce/
119
+ ```
120
+
121
+ Keep project-owned browser tests outside those reserved paths, for example under `e2e/specs/my-project/`; upgrade overlays framework files without deleting project test paths. Projects whose previous upgrader predates this E2E synchronization may need to run `npm run upgrade` once more: the updated upgrader recognizes an incomplete Basic E2E baseline even when the version marker is already current.
122
+
109
123
  ## 7. Next steps for framework-aware development
110
124
 
111
125
  If you are contributing to framework-aware workflows or using Cabloy CLI generation directly, prefer CLI-backed generation over manual scaffolding.
@@ -9,20 +9,99 @@ The root `package.json` is the first reference point for shared monorepo workflo
9
9
  ## Current shared entrypoints in Cabloy Basic
10
10
 
11
11
  - `npm run init`
12
+ - `npm run upgrade`
13
+ - `npm run upgrade:dry-run`
12
14
  - `npm run vona`
13
15
  - `npm run zova`
14
16
  - `npm run dev`
15
17
  - `npm run dev:zova:admin`
16
18
  - `npm run dev:zova:web`
19
+ - `npm run dev:zova:commerce:web`
20
+ - `npm run dev:zova:commerce:admin`
17
21
  - `npm run build`
18
22
  - `npm run build:zova`
23
+ - `npm run build:zova:commerce`
24
+ - `npm run build:zova:commerce:web`
25
+ - `npm run build:zova:commerce:admin`
19
26
  - `npm run start`
20
27
  - `npm run test`
28
+ - `npm run test:e2e:basic`
29
+ - `npm run test:e2e:basic:web`
30
+ - `npm run test:e2e:basic:admin`
31
+ - `npm run test:e2e:basic:clean`
32
+ - `npm run test:e2e:commerce`
33
+ - `npm run test:e2e:commerce:web`
34
+ - `npm run test:e2e:commerce:admin`
35
+ - `npm run test:e2e:commerce:clean`
21
36
  - `npm run tsc`
22
37
  - `npm run docs:dev`
23
38
  - `npm run docs:build`
24
39
  - `npm run docs:preview`
25
40
 
41
+ ## Upgrade
42
+
43
+ Run `npm run upgrade:dry-run` before `npm run upgrade` to inspect the framework files and root manifest entries that an upgrade would synchronize. In Cabloy Basic, the upgrade owns `e2e/config/`, `e2e/scripts/`, `e2e/specs/a-basic/`, and `e2e/specs/a-commerce/`, together with the framework `test:e2e:*` scripts and `@playwright/test` development dependency. Keep project browser tests outside those reserved paths; see [Fullstack Quickstart](/fullstack/quickstart) for the upgrade bootstrap behavior.
44
+
45
+ ## SSR browser checks
46
+
47
+ Both E2E families use the same command structure:
48
+
49
+ - `test:e2e:<suite>` runs every browser scenario in the suite.
50
+ - `test:e2e:<suite>:web` and `test:e2e:<suite>:admin` are durable surface shortcuts that select `@web` and `@admin` tests.
51
+ - `test:e2e:<suite>:clean` resets managed local test state, starts one development Vona worker, then runs the suite or a Playwright-filtered subset.
52
+
53
+ Use native Playwright tags for scenario categories rather than adding a root script for each feature. Every framework scenario uses one surface tag (`@web` or `@admin`) and one purpose tag when applicable (`@smoke` or `@flow`). ATP IDs remain in titles for exact evidence and failure reruns.
54
+
55
+ Pass Playwright options after npm's `--` delimiter:
56
+
57
+ ```bash
58
+ # Exact acceptance scenario
59
+ npm run test:e2e:basic -- --grep ATP-BASIC-FLOW-01
60
+
61
+ # Category or surface selection
62
+ npm run test:e2e:basic:clean -- --grep @flow
63
+ npm run test:e2e:basic:clean -- --grep @admin
64
+
65
+ # Compose tags with a Playwright regular expression
66
+ npm run test:e2e:basic:clean -- --grep '(?=.*@admin)(?=.*@flow)'
67
+ ```
68
+
69
+ The managed `:clean` runner owns its suite config and local lifecycle. It accepts normal Playwright selection and reporting options, but does not accept `--config` or positional spec paths. Use `--grep` or `--grep-invert` to narrow the run. It rejects an external base URL rather than resetting or starting an externally managed target.
70
+
71
+ The Basic suite exercises Web at `/` and Admin at `/admin` through Vona's SSR dispatcher. Prepare fresh Basic SSR artifacts explicitly when frontend SSR output has changed:
72
+
73
+ ```bash
74
+ npm run build:zova
75
+ npm run deps:vona
76
+ npm run test:e2e:basic:clean
77
+ ```
78
+
79
+ For an externally managed Basic target, set `BASIC_E2E_BASE_URL` and use aggregate, surface, or forwarded-tag commands. These commands do not reset, start, stop, or rebuild the target:
80
+
81
+ ```bash
82
+ BASIC_E2E_BASE_URL=http://127.0.0.1:7102 npm run test:e2e:basic
83
+ BASIC_E2E_BASE_URL=http://127.0.0.1:7102 npm run test:e2e:basic:admin
84
+ BASIC_E2E_BASE_URL=http://127.0.0.1:7102 npm run test:e2e:basic -- --grep @flow
85
+ ```
86
+
87
+ Commerce browser acceptance exercises Customer Web at `/commerce` and Operator Admin routing at `/commerce-admin`. Prepare its paired artifacts explicitly:
88
+
89
+ ```bash
90
+ npm run build:zova:commerce
91
+ npm run deps:vona
92
+ npm run test:e2e:commerce:clean
93
+ ```
94
+
95
+ For an externally managed Commerce target, set `COMMERCE_E2E_BASE_URL` and use the matching aggregate, surface, or forwarded-tag command. The target owner is responsible for data, cache, and artifact freshness:
96
+
97
+ ```bash
98
+ COMMERCE_E2E_BASE_URL=http://127.0.0.1:7102 npm run test:e2e:commerce
99
+ COMMERCE_E2E_BASE_URL=http://127.0.0.1:7102 npm run test:e2e:commerce:web
100
+ COMMERCE_E2E_BASE_URL=http://127.0.0.1:7102 npm run test:e2e:commerce -- --grep @smoke
101
+ ```
102
+
103
+ Browser commands consume existing SSR and REST artifacts; they never rebuild them. Install Chromium once when needed with `npx playwright install chromium`.
104
+
26
105
  ## Edition-sensitive note
27
106
 
28
107
  Cabloy Start keeps the same high-level pattern while using different frontend flavors such as `cabloyStartAdmin` and `cabloyStartWeb`, plus its own SSR site baselines and project assets in the licensed private repository.
@@ -0,0 +1,3 @@
1
+ import { createE2eConfig } from './playwright.shared.config.ts';
2
+
3
+ export default createE2eConfig('basic');
@@ -0,0 +1,3 @@
1
+ import { createE2eConfig } from './playwright.shared.config.ts';
2
+
3
+ export default createE2eConfig('commerce');
@@ -0,0 +1,38 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ import type { E2eSuiteName } from '../scripts/e2e.ts';
4
+
5
+ import { E2E_LOCAL_BASE_URL, E2E_ROOT_DIR, getE2eSuite } from '../scripts/e2e.ts';
6
+
7
+ export function createE2eConfig(suiteName: E2eSuiteName) {
8
+ const suite = getE2eSuite(suiteName);
9
+ const externalBaseURL = process.env[suite.externalBaseUrlEnv];
10
+ const baseURL = externalBaseURL || E2E_LOCAL_BASE_URL;
11
+
12
+ return defineConfig({
13
+ testDir: suite.testDir,
14
+ fullyParallel: true,
15
+ forbidOnly: !!process.env.CI,
16
+ retries: process.env.CI ? 2 : 0,
17
+ reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'list',
18
+ use: {
19
+ baseURL,
20
+ trace: 'on-first-retry',
21
+ },
22
+ webServer: externalBaseURL
23
+ ? undefined
24
+ : {
25
+ command: 'node e2e/scripts/startE2eVona.ts',
26
+ cwd: E2E_ROOT_DIR,
27
+ url: `${baseURL}${suite.readinessPath}`,
28
+ timeout: 180_000,
29
+ reuseExistingServer: false,
30
+ stdout: 'pipe',
31
+ stderr: 'pipe',
32
+ gracefulShutdown: {
33
+ signal: 'SIGINT',
34
+ timeout: 10_000,
35
+ },
36
+ },
37
+ });
38
+ }
@@ -0,0 +1,33 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ export const E2E_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
5
+ export const E2E_PORT = 7102;
6
+ export const E2E_LOCAL_BASE_URL = `http://127.0.0.1:${E2E_PORT}`;
7
+
8
+ const E2E_CONFIG_DIR = resolve(E2E_ROOT_DIR, 'e2e', 'config');
9
+ const E2E_SPECS_DIR = resolve(E2E_ROOT_DIR, 'e2e', 'specs');
10
+
11
+ const e2eSuites = {
12
+ basic: {
13
+ externalBaseUrlEnv: 'BASIC_E2E_BASE_URL',
14
+ configFile: resolve(E2E_CONFIG_DIR, 'playwright.basic.config.ts'),
15
+ testDir: resolve(E2E_SPECS_DIR, 'a-basic'),
16
+ readinessPath: '/',
17
+ },
18
+ commerce: {
19
+ externalBaseUrlEnv: 'COMMERCE_E2E_BASE_URL',
20
+ configFile: resolve(E2E_CONFIG_DIR, 'playwright.commerce.config.ts'),
21
+ testDir: resolve(E2E_SPECS_DIR, 'a-commerce'),
22
+ readinessPath: '/commerce',
23
+ },
24
+ } as const;
25
+
26
+ export type E2eSuiteName = keyof typeof e2eSuites;
27
+
28
+ export function getE2eSuite(name: string | undefined) {
29
+ if (name && name in e2eSuites) {
30
+ return e2eSuites[name as E2eSuiteName];
31
+ }
32
+ throw new Error(`Expected an E2E suite: ${Object.keys(e2eSuites).join(', ')}.`);
33
+ }
@@ -0,0 +1,38 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ import { E2E_ROOT_DIR } from './e2e.ts';
4
+
5
+ const child = spawn('npm', ['run', 'dev:one'], {
6
+ cwd: E2E_ROOT_DIR,
7
+ detached: process.platform !== 'win32',
8
+ stdio: 'inherit',
9
+ });
10
+
11
+ let stopping = false;
12
+
13
+ function stop() {
14
+ if (stopping) return;
15
+ stopping = true;
16
+ if (process.platform === 'win32') {
17
+ child.kill('SIGINT');
18
+ } else if (child.pid) {
19
+ try {
20
+ process.kill(-child.pid, 'SIGINT');
21
+ } catch (error: any) {
22
+ if (error.code !== 'ESRCH') throw error;
23
+ }
24
+ }
25
+ }
26
+
27
+ process.on('SIGINT', stop);
28
+ process.on('SIGTERM', stop);
29
+
30
+ child.on('error', error => {
31
+ // eslint-disable-next-line
32
+ console.error(error);
33
+ process.exitCode = 1;
34
+ });
35
+
36
+ child.on('exit', code => {
37
+ process.exitCode = code ?? 1;
38
+ });
@@ -0,0 +1,56 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createServer } from 'node:net';
3
+
4
+ import { E2E_PORT, E2E_ROOT_DIR, getE2eSuite } from './e2e.ts';
5
+
6
+ const suiteName = process.argv[2];
7
+ const suite = getE2eSuite(suiteName);
8
+ const playwrightArgs = process.argv.slice(3);
9
+
10
+ function run(command: string, args: string[], cwd = E2E_ROOT_DIR) {
11
+ // eslint-disable-next-line
12
+ console.log(`\n===> ${[command, ...args].join(' ')}`);
13
+ execFileSync(command, args, {
14
+ cwd,
15
+ stdio: 'inherit',
16
+ });
17
+ }
18
+
19
+ function assertPlaywrightArgs(args: string[]) {
20
+ for (const arg of args) {
21
+ if (arg === '--config' || arg.startsWith('--config=')) {
22
+ throw new Error(
23
+ `test:e2e:${suiteName}:clean manages its suite config. Use --grep or --grep-invert to select tests.`,
24
+ );
25
+ }
26
+ if (!arg.startsWith('-') && (arg.startsWith('e2e/') || /\.[cm]?[jt]sx?$/.test(arg))) {
27
+ throw new Error(
28
+ `test:e2e:${suiteName}:clean does not accept spec paths. Use --grep or --grep-invert to select tests.`,
29
+ );
30
+ }
31
+ }
32
+ }
33
+
34
+ async function assertPortAvailable() {
35
+ const server = createServer();
36
+ await new Promise<void>((resolvePromise, reject) => {
37
+ server.once('error', reject);
38
+ server.listen(E2E_PORT, '127.0.0.1', () => {
39
+ server.close(error => {
40
+ if (error) reject(error);
41
+ else resolvePromise();
42
+ });
43
+ });
44
+ });
45
+ }
46
+
47
+ if (process.env[suite.externalBaseUrlEnv]) {
48
+ throw new Error(
49
+ `test:e2e:${suiteName}:clean manages only a local target. Use the aggregate or surface E2E commands for ${suite.externalBaseUrlEnv}.`,
50
+ );
51
+ }
52
+
53
+ assertPlaywrightArgs(playwrightArgs);
54
+ await assertPortAvailable();
55
+ run('npm', ['run', 'db:reset']);
56
+ run('npx', ['playwright', 'test', '--config', suite.configFile, ...playwrightArgs]);