@voxgig/build 4.4.0 → 4.12.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 (100) hide show
  1. package/README.md +51 -2
  2. package/build.ts +20 -0
  3. package/dist/api/api_gen.d.ts +7 -0
  4. package/dist/api/api_gen.js +370 -0
  5. package/dist/api/api_gen.js.map +1 -0
  6. package/dist/build.d.ts +16 -1
  7. package/dist/build.js +17 -1
  8. package/dist/build.js.map +1 -1
  9. package/dist/doc/doc_gen.d.ts +8 -0
  10. package/dist/doc/doc_gen.js +397 -0
  11. package/dist/doc/doc_gen.js.map +1 -0
  12. package/dist/env/web/web_gen.js +197 -11
  13. package/dist/env/web/web_gen.js.map +1 -1
  14. package/dist/shape/ent.js +4 -2
  15. package/dist/shape/ent.js.map +1 -1
  16. package/env/web/web_gen.ts +219 -13
  17. package/package.json +6 -9
  18. package/tm/web/backend/env/shared/seed.ts.frag +23 -0
  19. package/tm/web/backend/env/web/api.ts.frag +157 -0
  20. package/tm/web/backend/env/web/web.ts.frag +43 -8
  21. package/tm/web/backend/srv/api/api-srv.ts.frag +4 -0
  22. package/tm/web/backend/srv/api/expose.ts.frag +33 -0
  23. package/tm/web/backend/srv/api/get_info.ts.frag +6 -0
  24. package/tm/web/backend/srv/api/on_ent.ts.frag +124 -0
  25. package/tm/web/backend/srv/auth/apikey_util.ts.frag +10 -0
  26. package/tm/web/backend/srv/auth/auth-srv.ts.frag +1 -0
  27. package/tm/web/backend/srv/auth/change_pass.ts.frag +12 -0
  28. package/tm/web/backend/srv/auth/create_apikey.ts.frag +33 -0
  29. package/tm/web/backend/srv/auth/get_info.ts.frag +1 -0
  30. package/tm/web/backend/srv/auth/list_apikey.ts.frag +15 -0
  31. package/tm/web/backend/srv/auth/load_auth.ts.frag +10 -1
  32. package/tm/web/backend/srv/auth/remind_pass.ts.frag +35 -0
  33. package/tm/web/backend/srv/auth/revoke_apikey.ts.frag +23 -0
  34. package/tm/web/backend/srv/auth/signin_user.ts.frag +12 -2
  35. package/tm/web/backend/srv/auth/signout_user.ts.frag +11 -1
  36. package/tm/web/backend/srv/auth/update_user.ts.frag +15 -0
  37. package/tm/web/backend/srv/auth/user_util.ts.frag +21 -0
  38. package/tm/web/backend/srv/auth/web_change_pass.ts.frag +16 -0
  39. package/tm/web/backend/srv/auth/web_create_apikey.ts.frag +15 -0
  40. package/tm/web/backend/srv/auth/web_list_apikey.ts.frag +11 -0
  41. package/tm/web/backend/srv/auth/web_load_auth.ts.frag +1 -0
  42. package/tm/web/backend/srv/auth/web_remind_pass.ts.frag +7 -0
  43. package/tm/web/backend/srv/auth/web_revoke_apikey.ts.frag +14 -0
  44. package/tm/web/backend/srv/auth/web_signin_user.ts.frag +1 -0
  45. package/tm/web/backend/srv/auth/web_signout_user.ts.frag +11 -3
  46. package/tm/web/backend/srv/auth/web_update_user.ts.frag +11 -0
  47. package/tm/web/backend/srv/ent/access.ts.frag +179 -0
  48. package/tm/web/backend/srv/ent/cmd_list.ts.frag +86 -0
  49. package/tm/web/backend/srv/ent/cmd_load.ts.frag +56 -0
  50. package/tm/web/backend/srv/ent/cmd_remove.ts.frag +60 -0
  51. package/tm/web/backend/srv/ent/cmd_save.ts.frag +141 -0
  52. package/tm/web/backend/srv/ent/ent-srv.ts.frag +4 -0
  53. package/tm/web/backend/srv/ent/get_info.ts.frag +6 -0
  54. package/tm/web/backend/srv/ent/web_cmd_list.ts.frag +17 -0
  55. package/tm/web/backend/srv/ent/web_cmd_load.ts.frag +17 -0
  56. package/tm/web/backend/srv/ent/web_cmd_remove.ts.frag +17 -0
  57. package/tm/web/backend/srv/ent/web_cmd_save.ts.frag +17 -0
  58. package/tm/web/backend/test/unit/env/web/api-router.test.ts.frag +315 -0
  59. package/tm/web/backend/test/unit/env/web/surface.test.ts.frag +109 -0
  60. package/tm/web/backend/test/unit/srv/api/api.setup.ts.frag +162 -0
  61. package/tm/web/backend/test/unit/srv/api/api.test.ts.frag +225 -0
  62. package/tm/web/backend/test/unit/srv/auth/apikey.test.ts.frag +152 -0
  63. package/tm/web/backend/test/unit/srv/auth/auth.setup.ts.frag +40 -0
  64. package/tm/web/backend/test/unit/srv/auth/session.test.ts.frag +189 -0
  65. package/tm/web/backend/test/unit/srv/ent/ent.setup.ts.frag +53 -0
  66. package/tm/web/backend/test/unit/srv/ent/proxy.test.ts.frag +109 -0
  67. package/tm/web/docs/explanation/web-architecture.md.frag +50 -0
  68. package/tm/web/docs/how-to/add-a-custom-entity-view.md.frag +53 -0
  69. package/tm/web/docs/how-to/change-the-theme.md.frag +63 -0
  70. package/tm/web/docs/how-to/customise-the-web-app.md.frag +68 -0
  71. package/tm/web/docs/how-to/use-the-api.md.frag +53 -0
  72. package/tm/web/docs/reference/web-app.md.frag +98 -0
  73. package/tm/web/web/AGENTS.md.frag +52 -0
  74. package/tm/web/web/e2e/smoke.spec.js.frag +13 -11
  75. package/tm/web/web/index.html.frag +1 -0
  76. package/tm/web/web/package.json.frag +1 -0
  77. package/tm/web/web/playwright.config.js.frag +2 -1
  78. package/tm/web/web/src/api.js.frag +92 -0
  79. package/tm/web/web/src/bus.js.frag +14 -8
  80. package/tm/web/web/src/cmp/admin.js.frag +293 -212
  81. package/tm/web/web/src/cmp/admin.md.frag +51 -0
  82. package/tm/web/web/src/cmp/app.js.frag +12 -15
  83. package/tm/web/web/src/cmp/app.md.frag +28 -0
  84. package/tm/web/web/src/cmp/auth.js.frag +69 -41
  85. package/tm/web/web/src/cmp/auth.md.frag +36 -0
  86. package/tm/web/web/src/cmp/public.js.frag +58 -0
  87. package/tm/web/web/src/cmp/public.md.frag +26 -0
  88. package/tm/web/web/src/cmp/settings.js.frag +178 -0
  89. package/tm/web/web/src/cmp/settings.md.frag +30 -0
  90. package/tm/web/web/src/cmp/shell.js.frag +231 -0
  91. package/tm/web/web/src/cmp/shell.md.frag +49 -0
  92. package/tm/web/web/src/cmp/view/custom-view.js.frag +47 -0
  93. package/tm/web/web/src/cmp/view/custom-view.md.frag +31 -0
  94. package/tm/web/web/src/custom.css.frag +8 -0
  95. package/tm/web/web/src/customise.js.frag +30 -0
  96. package/tm/web/web/src/hooks.js.frag +79 -0
  97. package/tm/web/web/src/main.js.frag +17 -4
  98. package/tm/web/web/src/model.js.frag +162 -0
  99. package/tm/web/web/src/style.css.frag +135 -21
  100. package/tm/web/web/src/theme.js.frag +82 -0
@@ -26,20 +26,52 @@ const generate_1 = require("../lambda/generate");
26
26
  // the project root, which holds backend/ and web/). $$slots$$ in the
27
27
  // output path are rendered too.
28
28
  const WEB_FILES = [
29
- // Frontend SPA (web/)
30
- { frag: 'web/package.json.frag', out: 'web/package.json' },
31
- { frag: 'web/index.html.frag', out: 'web/index.html' },
32
- { frag: 'web/playwright.config.js.frag', out: 'web/playwright.config.js' },
33
- { frag: 'web/src/main.js.frag', out: 'web/src/main.js' },
34
- { frag: 'web/src/bus.js.frag', out: 'web/src/bus.js' },
35
- { frag: 'web/src/style.css.frag', out: 'web/src/style.css' },
36
- { frag: 'web/src/cmp/app.js.frag', out: 'web/src/cmp/app.js' },
37
- { frag: 'web/src/cmp/auth.js.frag', out: 'web/src/cmp/auth.js' },
38
- { frag: 'web/src/cmp/admin.js.frag', out: 'web/src/cmp/admin.js' },
39
- { frag: 'web/e2e/smoke.spec.js.frag', out: 'web/e2e/smoke.spec.js' },
29
+ // Frontend SPA (web/) — a model-driven enterprise app: public site + login,
30
+ // an app shell (nav / project selector / user menu / collapsible entity
31
+ // menu), and generic CRUD with relationship navigation. All runtime
32
+ // model-driven (model.js reads /model.json), so it fits any entity graph.
33
+ { frag: 'web/package.json.frag', out: '$$fe$$/package.json' },
34
+ { frag: 'web/index.html.frag', out: '$$fe$$/index.html' },
35
+ { frag: 'web/playwright.config.js.frag', out: '$$fe$$/playwright.config.js' },
36
+ { frag: 'web/src/main.js.frag', out: '$$fe$$/src/main.js' },
37
+ { frag: 'web/src/bus.js.frag', out: '$$fe$$/src/bus.js' },
38
+ { frag: 'web/src/model.js.frag', out: '$$fe$$/src/model.js' },
39
+ { frag: 'web/src/api.js.frag', out: '$$fe$$/src/api.js' },
40
+ { frag: 'web/src/hooks.js.frag', out: '$$fe$$/src/hooks.js' },
41
+ { frag: 'web/src/theme.js.frag', out: '$$fe$$/src/theme.js' },
42
+ { frag: 'web/src/customise.js.frag', out: '$$fe$$/src/customise.js' },
43
+ { frag: 'web/src/custom.css.frag', out: '$$fe$$/src/custom.css' },
44
+ { frag: 'web/src/style.css.frag', out: '$$fe$$/src/style.css' },
45
+ { frag: 'web/src/cmp/app.js.frag', out: '$$fe$$/src/cmp/app.js' },
46
+ { frag: 'web/src/cmp/public.js.frag', out: '$$fe$$/src/cmp/public.js' },
47
+ { frag: 'web/src/cmp/auth.js.frag', out: '$$fe$$/src/cmp/auth.js' },
48
+ { frag: 'web/src/cmp/shell.js.frag', out: '$$fe$$/src/cmp/shell.js' },
49
+ { frag: 'web/src/cmp/admin.js.frag', out: '$$fe$$/src/cmp/admin.js' },
50
+ { frag: 'web/src/cmp/settings.js.frag', out: '$$fe$$/src/cmp/settings.js' },
51
+ // Per-component doc sidecars (mermaid structure + message diagrams),
52
+ // create-once next to each component.
53
+ { frag: 'web/src/cmp/app.md.frag', out: '$$fe$$/src/cmp/app.md' },
54
+ { frag: 'web/src/cmp/public.md.frag', out: '$$fe$$/src/cmp/public.md' },
55
+ { frag: 'web/src/cmp/auth.md.frag', out: '$$fe$$/src/cmp/auth.md' },
56
+ { frag: 'web/src/cmp/shell.md.frag', out: '$$fe$$/src/cmp/shell.md' },
57
+ { frag: 'web/src/cmp/admin.md.frag', out: '$$fe$$/src/cmp/admin.md' },
58
+ { frag: 'web/src/cmp/settings.md.frag', out: '$$fe$$/src/cmp/settings.md' },
59
+ { frag: 'web/e2e/smoke.spec.js.frag', out: '$$fe$$/e2e/smoke.spec.js' },
60
+ { frag: 'web/AGENTS.md.frag', out: '$$fe$$/AGENTS.md' },
61
+ // Project docs (Diátaxis) for the web app, alongside the scaffold's
62
+ // backend docs (@voxgig/create-system DocsPart). Create-once like all
63
+ // WEB_FILES: the project owns its docs after generation.
64
+ { frag: 'docs/how-to/customise-the-web-app.md.frag', out: 'docs/how-to/customise-the-web-app.md' },
65
+ { frag: 'docs/how-to/use-the-api.md.frag', out: 'docs/how-to/use-the-api.md' },
66
+ { frag: 'docs/how-to/change-the-theme.md.frag', out: 'docs/how-to/change-the-theme.md' },
67
+ { frag: 'docs/how-to/add-a-custom-entity-view.md.frag', out: 'docs/how-to/add-a-custom-entity-view.md' },
68
+ { frag: 'docs/reference/web-app.md.frag', out: 'docs/reference/web-app.md' },
69
+ { frag: 'docs/explanation/web-architecture.md.frag', out: 'docs/explanation/web-architecture.md' },
40
70
  // Backend web runner + auth service (backend/src/...)
41
71
  { frag: 'backend/env/web/web.ts.frag', out: 'backend/src/env/web/web.ts' },
72
+ { frag: 'backend/env/shared/seed.ts.frag', out: 'backend/src/env/shared/seed.ts' },
42
73
  { frag: 'backend/srv/auth/auth-srv.ts.frag', out: 'backend/src/srv/auth/auth-srv.ts' },
74
+ { frag: 'backend/srv/auth/user_util.ts.frag', out: 'backend/src/srv/auth/user_util.ts' },
43
75
  { frag: 'backend/srv/auth/get_info.ts.frag', out: 'backend/src/srv/auth/get_info.ts' },
44
76
  { frag: 'backend/srv/auth/signin_user.ts.frag', out: 'backend/src/srv/auth/signin_user.ts' },
45
77
  { frag: 'backend/srv/auth/signout_user.ts.frag', out: 'backend/src/srv/auth/signout_user.ts' },
@@ -47,6 +79,56 @@ const WEB_FILES = [
47
79
  { frag: 'backend/srv/auth/web_signin_user.ts.frag', out: 'backend/src/srv/auth/web_signin_user.ts' },
48
80
  { frag: 'backend/srv/auth/web_signout_user.ts.frag', out: 'backend/src/srv/auth/web_signout_user.ts' },
49
81
  { frag: 'backend/srv/auth/web_load_auth.ts.frag', out: 'backend/src/srv/auth/web_load_auth.ts' },
82
+ // Auth settings/security extensions.
83
+ { frag: 'backend/srv/auth/change_pass.ts.frag', out: 'backend/src/srv/auth/change_pass.ts' },
84
+ { frag: 'backend/srv/auth/web_change_pass.ts.frag', out: 'backend/src/srv/auth/web_change_pass.ts' },
85
+ { frag: 'backend/srv/auth/update_user.ts.frag', out: 'backend/src/srv/auth/update_user.ts' },
86
+ { frag: 'backend/srv/auth/web_update_user.ts.frag', out: 'backend/src/srv/auth/web_update_user.ts' },
87
+ { frag: 'backend/srv/auth/remind_pass.ts.frag', out: 'backend/src/srv/auth/remind_pass.ts' },
88
+ { frag: 'backend/srv/auth/web_remind_pass.ts.frag', out: 'backend/src/srv/auth/web_remind_pass.ts' },
89
+ // API access keys (Settings & security + REST API auth).
90
+ { frag: 'backend/srv/auth/apikey_util.ts.frag', out: 'backend/src/srv/auth/apikey_util.ts' },
91
+ { frag: 'backend/srv/auth/create_apikey.ts.frag', out: 'backend/src/srv/auth/create_apikey.ts' },
92
+ { frag: 'backend/srv/auth/list_apikey.ts.frag', out: 'backend/src/srv/auth/list_apikey.ts' },
93
+ { frag: 'backend/srv/auth/revoke_apikey.ts.frag', out: 'backend/src/srv/auth/revoke_apikey.ts' },
94
+ { frag: 'backend/srv/auth/web_create_apikey.ts.frag', out: 'backend/src/srv/auth/web_create_apikey.ts' },
95
+ { frag: 'backend/srv/auth/web_list_apikey.ts.frag', out: 'backend/src/srv/auth/web_list_apikey.ts' },
96
+ { frag: 'backend/srv/auth/web_revoke_apikey.ts.frag', out: 'backend/src/srv/auth/web_revoke_apikey.ts' },
97
+ // The strict-JSON REST API (model main.api): api service + HTTP mapping.
98
+ // valid_gen.ts (request validation shapes) is REGENERATED by api_gen.
99
+ { frag: 'backend/srv/api/api-srv.ts.frag', out: 'backend/src/srv/api/api-srv.ts' },
100
+ { frag: 'backend/srv/api/get_info.ts.frag', out: 'backend/src/srv/api/get_info.ts' },
101
+ { frag: 'backend/srv/api/on_ent.ts.frag', out: 'backend/src/srv/api/on_ent.ts' },
102
+ { frag: 'backend/srv/api/expose.ts.frag', out: 'backend/src/srv/api/expose.ts' },
103
+ { frag: 'backend/env/web/api.ts.frag', out: 'backend/src/env/web/api.ts' },
104
+ // Unit tests for what EnvWeb generates (node:test). They are
105
+ // MODEL-DRIVEN - the entity under test and its payloads come from the
106
+ // model - so they hold for any entity graph, and they stand in for
107
+ // other services with mock messages rather than booting them.
108
+ { frag: 'backend/test/unit/srv/api/api.setup.ts.frag', out: 'backend/test/unit/srv/api/api.setup.ts' },
109
+ { frag: 'backend/test/unit/srv/api/api.test.ts.frag', out: 'backend/test/unit/srv/api/api.test.ts' },
110
+ { frag: 'backend/test/unit/srv/auth/auth.setup.ts.frag', out: 'backend/test/unit/srv/auth/auth.setup.ts' },
111
+ { frag: 'backend/test/unit/srv/auth/apikey.test.ts.frag', out: 'backend/test/unit/srv/auth/apikey.test.ts' },
112
+ { frag: 'backend/test/unit/srv/auth/session.test.ts.frag', out: 'backend/test/unit/srv/auth/session.test.ts' },
113
+ { frag: 'backend/test/unit/env/web/api-router.test.ts.frag', out: 'backend/test/unit/env/web/api-router.test.ts' },
114
+ { frag: 'backend/test/unit/env/web/surface.test.ts.frag', out: 'backend/test/unit/env/web/surface.test.ts' },
115
+ { frag: 'backend/test/unit/srv/ent/ent.setup.ts.frag', out: 'backend/test/unit/srv/ent/ent.setup.ts' },
116
+ { frag: 'backend/test/unit/srv/ent/proxy.test.ts.frag', out: 'backend/test/unit/srv/ent/proxy.test.ts' },
117
+ // Generic entity service (parameterised CRUD, membership-scoped).
118
+ { frag: 'backend/srv/ent/ent-srv.ts.frag', out: 'backend/src/srv/ent/ent-srv.ts' },
119
+ { frag: 'backend/srv/ent/access.ts.frag', out: 'backend/src/srv/ent/access.ts' },
120
+ { frag: 'backend/srv/ent/get_info.ts.frag', out: 'backend/src/srv/ent/get_info.ts' },
121
+ { frag: 'backend/srv/ent/cmd_list.ts.frag', out: 'backend/src/srv/ent/cmd_list.ts' },
122
+ { frag: 'backend/srv/ent/cmd_load.ts.frag', out: 'backend/src/srv/ent/cmd_load.ts' },
123
+ { frag: 'backend/srv/ent/cmd_save.ts.frag', out: 'backend/src/srv/ent/cmd_save.ts' },
124
+ { frag: 'backend/srv/ent/cmd_remove.ts.frag', out: 'backend/src/srv/ent/cmd_remove.ts' },
125
+ // Browser proxies: aim:web,on:ent,cmd:* -> aim:ent,cmd:* (the gateway
126
+ // accepts only aim:web, so the entity service is never posted directly
127
+ // from a browser).
128
+ { frag: 'backend/srv/ent/web_cmd_list.ts.frag', out: 'backend/src/srv/ent/web_cmd_list.ts' },
129
+ { frag: 'backend/srv/ent/web_cmd_load.ts.frag', out: 'backend/src/srv/ent/web_cmd_load.ts' },
130
+ { frag: 'backend/srv/ent/web_cmd_save.ts.frag', out: 'backend/src/srv/ent/web_cmd_save.ts' },
131
+ { frag: 'backend/srv/ent/web_cmd_remove.ts.frag', out: 'backend/src/srv/ent/web_cmd_remove.ts' },
50
132
  ];
51
133
  exports.WEB_FILES = WEB_FILES;
52
134
  // Default seeded users (local testing only), rendered into the runner.
@@ -55,12 +137,39 @@ const DEFAULT_USERS = [
55
137
  { name: 'Bob Example', email: 'bob@example.com', password: 'bob-pass-01' },
56
138
  ];
57
139
  exports.DEFAULT_USERS = DEFAULT_USERS;
140
+ // The frontend folder name, from the model env entry (`web: { dir: ... }`).
141
+ // Defaults to 'web', so a project that says nothing keeps the layout it
142
+ // already has. Everything that names the folder - the WEB_FILES output
143
+ // paths, views.js, theme.css, and the static path the backend runner
144
+ // serves - goes through this one value, via the $$fe$$ slot.
145
+ //
146
+ // Only the FOLDER moves. The `aim:web` message namespace is unrelated and
147
+ // must never follow it: the gateway allow-list is the literal
148
+ // { 'aim:web': true }, and backend/src/env/web/ is the backend runner, not
149
+ // the SPA.
150
+ const DEFAULT_FE = 'web';
151
+ // Absent means 'web'. ANYTHING ELSE provided is validated rather than
152
+ // silently defaulted - `dir: ''` is a mistake, not a request for 'web', and
153
+ // defaulting it would generate the whole SPA into the project root.
154
+ function feDir(envdef) {
155
+ const dir = null == envdef ? undefined : envdef.dir;
156
+ if (null == dir) {
157
+ return DEFAULT_FE;
158
+ }
159
+ if ('string' !== typeof dir || '' === dir || dir.split('/').includes('..') ||
160
+ path_1.default.isAbsolute(dir)) {
161
+ throw new Error('EnvWeb: env web `dir` must be a non-empty relative folder name, got: ' +
162
+ JSON.stringify(dir));
163
+ }
164
+ return dir;
165
+ }
58
166
  // web_gen(model, spec): generate the frontend + backend web pieces into
59
167
  // the project. spec.root is the project root (parent of backend/); files
60
168
  // that already exist are left untouched (create-once).
61
169
  const web_gen = async (model, spec) => {
62
170
  const core = (0, conf_1.CoreConfShape)(model.main.conf.core);
63
171
  const envdef = spec.env || {};
172
+ const fe = feDir(envdef);
64
173
  const users = envdef.users || DEFAULT_USERS;
65
174
  const seed = users[0] || DEFAULT_USERS[0];
66
175
  const port = envdef.port ||
@@ -68,6 +177,7 @@ const web_gen = async (model, spec) => {
68
177
  const slots = {
69
178
  name: core.name,
70
179
  Name: (0, util_1.camelify)(core.name),
180
+ fe,
71
181
  users: JSON.stringify(users, null, 2),
72
182
  e2eport: envdef.e2eport || (port + 10),
73
183
  seedEmail: seed.email,
@@ -87,6 +197,82 @@ const web_gen = async (model, spec) => {
87
197
  fs_1.default.writeFileSync(dest, content);
88
198
  created.push(outrel);
89
199
  }
200
+ // Custom entity views: for every entity declaring `ux:{ view: 'custom' }`,
201
+ // generate a hand-coded view component (CREATE-ONCE, so edits survive) plus
202
+ // an always-regenerated index (views.js) that imports them.
203
+ const entzones = model.main.ent || {};
204
+ const customs = [];
205
+ for (const zone of Object.keys(entzones)) {
206
+ for (const name of Object.keys(entzones[zone])) {
207
+ const def = entzones[zone][name];
208
+ if (def && def.ux && 'custom' === def.ux.view) {
209
+ customs.push({ zone, name, canon: zone + '/' + name });
210
+ }
211
+ }
212
+ }
213
+ for (const c of customs) {
214
+ const vslots = Object.assign({}, slots, {
215
+ canon: c.canon,
216
+ zone: c.zone,
217
+ name: c.name,
218
+ tag: 'vg-view-' + c.zone + '-' + c.name,
219
+ className: 'VgView' + (0, util_1.camelify)(c.zone) + (0, util_1.camelify)(c.name),
220
+ Label: (0, util_1.camelify)(c.name),
221
+ });
222
+ // The component starter plus its doc sidecar, both create-once.
223
+ for (const ext of ['js', 'md']) {
224
+ const outrel = fe + '/src/cmp/view/' + c.zone + '_' + c.name + '.' + ext;
225
+ const dest = path_1.default.join(spec.root, outrel);
226
+ if (fs_1.default.existsSync(dest) && !spec.force) {
227
+ skipped.push(outrel);
228
+ continue;
229
+ }
230
+ const content = (0, generate_1.renderFragment)((0, generate_1.loadFragment)('web/src/cmp/view/custom-view.' + ext + '.frag', spec, 'web'), vslots);
231
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
232
+ fs_1.default.writeFileSync(dest, content);
233
+ created.push(outrel);
234
+ }
235
+ }
236
+ // views.js — the generated index of custom views. Regenerated whenever the
237
+ // set of custom views changes; a no-op run leaves it (and `created`) alone.
238
+ const viewsRel = fe + '/src/views.js';
239
+ const viewsPath = path_1.default.join(spec.root, viewsRel);
240
+ const viewsBody = '// AUTO-GENERATED: imports every custom entity view (model entities declaring\n' +
241
+ '// ux:{view:\'custom\'}). Regenerated from the model — do not edit. The view\n' +
242
+ '// files it imports are hand-coded and create-once.\n\n' +
243
+ customs.map((c) => `import './cmp/view/${c.zone}_${c.name}.js'`).join('\n') + '\n';
244
+ const existingViews = fs_1.default.existsSync(viewsPath) ? fs_1.default.readFileSync(viewsPath, 'utf8') : null;
245
+ if (existingViews !== viewsBody) {
246
+ fs_1.default.mkdirSync(path_1.default.join(spec.root, fe, 'src'), { recursive: true });
247
+ fs_1.default.writeFileSync(viewsPath, viewsBody);
248
+ created.push(viewsRel);
249
+ }
250
+ // theme.css — the design theme from the model (main.theme). Each mode's
251
+ // tokens become CSS variables under :root[data-theme-mode="<mode>"], with the
252
+ // default mode also on plain :root. Regenerated from the model; override
253
+ // tokens in custom.css and add modes via the theme:modes hook.
254
+ const theme = model.main && model.main.theme;
255
+ if (theme && theme.modes && Object.keys(theme.modes).length) {
256
+ const defMode = theme.mode || Object.keys(theme.modes)[0];
257
+ const tokensCss = (tokens) => Object.keys(tokens).sort()
258
+ .map((k) => ` --vg-${k}: ${tokens[k]};`).join('\n');
259
+ const blocks = [':root,\n:root[data-theme-mode="' + defMode + '"] {\n' + tokensCss(theme.modes[defMode]) + '\n}'];
260
+ for (const mode of Object.keys(theme.modes)) {
261
+ if (mode !== defMode) {
262
+ blocks.push(':root[data-theme-mode="' + mode + '"] {\n' + tokensCss(theme.modes[mode]) + '\n}');
263
+ }
264
+ }
265
+ const themeBody = '/* AUTO-GENERATED from the model theme (main.theme) — do not edit.\n' +
266
+ ' Override tokens in custom.css; add modes via the theme:modes hook. */\n\n' +
267
+ blocks.join('\n\n') + '\n';
268
+ const themePath = path_1.default.join(spec.root, fe, 'src/theme.css');
269
+ const existingTheme = fs_1.default.existsSync(themePath) ? fs_1.default.readFileSync(themePath, 'utf8') : null;
270
+ if (existingTheme !== themeBody) {
271
+ fs_1.default.mkdirSync(path_1.default.join(spec.root, fe, 'src'), { recursive: true });
272
+ fs_1.default.writeFileSync(themePath, themeBody);
273
+ created.push(fe + '/src/theme.css');
274
+ }
275
+ }
90
276
  return { created: created.sort(), skipped: skipped.sort() };
91
277
  };
92
278
  exports.web_gen = web_gen;
@@ -1 +1 @@
1
- {"version":3,"file":"web_gen.js","sourceRoot":"","sources":["../../../env/web/web_gen.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web_gen.js","sourceRoot":"","sources":["../../../env/web/web_gen.ts"],"names":[],"mappings":"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"}
package/dist/shape/ent.js CHANGED
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EntShape = void 0;
4
4
  const gubu_1 = require("gubu");
5
5
  const { Open, Skip } = gubu_1.Gubu;
6
- const EntShape = (0, gubu_1.Gubu)({
6
+ // Open: entities may carry extra attributes beyond the deployment-relevant
7
+ // ones below (e.g. `ux` for frontend view hints), which this generator ignores.
8
+ const EntShape = (0, gubu_1.Gubu)(Open({
7
9
  id: {
8
10
  field: 'id'
9
11
  },
@@ -23,6 +25,6 @@ const EntShape = (0, gubu_1.Gubu)({
23
25
  active: false
24
26
  }),
25
27
  custom: Skip(String),
26
- }, { prefix: 'Entity' });
28
+ }), { prefix: 'Entity' });
27
29
  exports.EntShape = EntShape;
28
30
  //# sourceMappingURL=ent.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ent.js","sourceRoot":"","sources":["../../shape/ent.ts"],"names":[],"mappings":";;;AACA,+BAA2B;AAE3B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAI,CAAA;AAG3B,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC;IACpB,EAAE,EAAE;QACF,KAAK,EAAE,IAAI;KACZ;IACD,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EACrB,CAAC;IACF,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;IACf,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EACrB,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC;QACb,IAAI,EAAE,EAAE;KACT,CAAC;IACF,MAAM,EAAE,IAAI,CAAC;QACX,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,EAAE;KACX,CAAC;IACF,KAAK,EAAE,IAAI,CAAC;QACV,MAAM,EAAE,KAAK;KACd,CAAC;IACF,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;AAItB,4BAAQ"}
1
+ {"version":3,"file":"ent.js","sourceRoot":"","sources":["../../shape/ent.ts"],"names":[],"mappings":";;;AACA,+BAA2B;AAE3B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAI,CAAA;AAG3B,2EAA2E;AAC3E,gFAAgF;AAChF,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC,IAAI,CAAC;IACzB,EAAE,EAAE;QACF,KAAK,EAAE,IAAI;KACZ;IACD,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EACrB,CAAC;IACF,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;IACf,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,EACrB,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC;QACb,IAAI,EAAE,EAAE;KACT,CAAC;IACF,MAAM,EAAE,IAAI,CAAC;QACX,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,EAAE;QACV,MAAM,EAAE,EAAE;KACX,CAAC;IACF,KAAK,EAAE,IAAI,CAAC;QACV,MAAM,EAAE,KAAK;KACd,CAAC;IACF,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;CACrB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;AAIvB,4BAAQ"}
@@ -27,21 +27,55 @@ import { loadFragment, renderFragment } from '../lambda/generate'
27
27
  // the project root, which holds backend/ and web/). $$slots$$ in the
28
28
  // output path are rendered too.
29
29
  const WEB_FILES: { frag: string, out: string }[] = [
30
- // Frontend SPA (web/)
31
- { frag: 'web/package.json.frag', out: 'web/package.json' },
32
- { frag: 'web/index.html.frag', out: 'web/index.html' },
33
- { frag: 'web/playwright.config.js.frag', out: 'web/playwright.config.js' },
34
- { frag: 'web/src/main.js.frag', out: 'web/src/main.js' },
35
- { frag: 'web/src/bus.js.frag', out: 'web/src/bus.js' },
36
- { frag: 'web/src/style.css.frag', out: 'web/src/style.css' },
37
- { frag: 'web/src/cmp/app.js.frag', out: 'web/src/cmp/app.js' },
38
- { frag: 'web/src/cmp/auth.js.frag', out: 'web/src/cmp/auth.js' },
39
- { frag: 'web/src/cmp/admin.js.frag', out: 'web/src/cmp/admin.js' },
40
- { frag: 'web/e2e/smoke.spec.js.frag', out: 'web/e2e/smoke.spec.js' },
30
+ // Frontend SPA (web/) — a model-driven enterprise app: public site + login,
31
+ // an app shell (nav / project selector / user menu / collapsible entity
32
+ // menu), and generic CRUD with relationship navigation. All runtime
33
+ // model-driven (model.js reads /model.json), so it fits any entity graph.
34
+ { frag: 'web/package.json.frag', out: '$$fe$$/package.json' },
35
+ { frag: 'web/index.html.frag', out: '$$fe$$/index.html' },
36
+ { frag: 'web/playwright.config.js.frag', out: '$$fe$$/playwright.config.js' },
37
+ { frag: 'web/src/main.js.frag', out: '$$fe$$/src/main.js' },
38
+ { frag: 'web/src/bus.js.frag', out: '$$fe$$/src/bus.js' },
39
+ { frag: 'web/src/model.js.frag', out: '$$fe$$/src/model.js' },
40
+ { frag: 'web/src/api.js.frag', out: '$$fe$$/src/api.js' },
41
+ { frag: 'web/src/hooks.js.frag', out: '$$fe$$/src/hooks.js' },
42
+ { frag: 'web/src/theme.js.frag', out: '$$fe$$/src/theme.js' },
43
+ { frag: 'web/src/customise.js.frag', out: '$$fe$$/src/customise.js' },
44
+ { frag: 'web/src/custom.css.frag', out: '$$fe$$/src/custom.css' },
45
+ { frag: 'web/src/style.css.frag', out: '$$fe$$/src/style.css' },
46
+ { frag: 'web/src/cmp/app.js.frag', out: '$$fe$$/src/cmp/app.js' },
47
+ { frag: 'web/src/cmp/public.js.frag', out: '$$fe$$/src/cmp/public.js' },
48
+ { frag: 'web/src/cmp/auth.js.frag', out: '$$fe$$/src/cmp/auth.js' },
49
+ { frag: 'web/src/cmp/shell.js.frag', out: '$$fe$$/src/cmp/shell.js' },
50
+ { frag: 'web/src/cmp/admin.js.frag', out: '$$fe$$/src/cmp/admin.js' },
51
+ { frag: 'web/src/cmp/settings.js.frag', out: '$$fe$$/src/cmp/settings.js' },
52
+
53
+ // Per-component doc sidecars (mermaid structure + message diagrams),
54
+ // create-once next to each component.
55
+ { frag: 'web/src/cmp/app.md.frag', out: '$$fe$$/src/cmp/app.md' },
56
+ { frag: 'web/src/cmp/public.md.frag', out: '$$fe$$/src/cmp/public.md' },
57
+ { frag: 'web/src/cmp/auth.md.frag', out: '$$fe$$/src/cmp/auth.md' },
58
+ { frag: 'web/src/cmp/shell.md.frag', out: '$$fe$$/src/cmp/shell.md' },
59
+ { frag: 'web/src/cmp/admin.md.frag', out: '$$fe$$/src/cmp/admin.md' },
60
+ { frag: 'web/src/cmp/settings.md.frag', out: '$$fe$$/src/cmp/settings.md' },
61
+ { frag: 'web/e2e/smoke.spec.js.frag', out: '$$fe$$/e2e/smoke.spec.js' },
62
+ { frag: 'web/AGENTS.md.frag', out: '$$fe$$/AGENTS.md' },
63
+
64
+ // Project docs (Diátaxis) for the web app, alongside the scaffold's
65
+ // backend docs (@voxgig/create-system DocsPart). Create-once like all
66
+ // WEB_FILES: the project owns its docs after generation.
67
+ { frag: 'docs/how-to/customise-the-web-app.md.frag', out: 'docs/how-to/customise-the-web-app.md' },
68
+ { frag: 'docs/how-to/use-the-api.md.frag', out: 'docs/how-to/use-the-api.md' },
69
+ { frag: 'docs/how-to/change-the-theme.md.frag', out: 'docs/how-to/change-the-theme.md' },
70
+ { frag: 'docs/how-to/add-a-custom-entity-view.md.frag', out: 'docs/how-to/add-a-custom-entity-view.md' },
71
+ { frag: 'docs/reference/web-app.md.frag', out: 'docs/reference/web-app.md' },
72
+ { frag: 'docs/explanation/web-architecture.md.frag', out: 'docs/explanation/web-architecture.md' },
41
73
 
42
74
  // Backend web runner + auth service (backend/src/...)
43
75
  { frag: 'backend/env/web/web.ts.frag', out: 'backend/src/env/web/web.ts' },
76
+ { frag: 'backend/env/shared/seed.ts.frag', out: 'backend/src/env/shared/seed.ts' },
44
77
  { frag: 'backend/srv/auth/auth-srv.ts.frag', out: 'backend/src/srv/auth/auth-srv.ts' },
78
+ { frag: 'backend/srv/auth/user_util.ts.frag', out: 'backend/src/srv/auth/user_util.ts' },
45
79
  { frag: 'backend/srv/auth/get_info.ts.frag', out: 'backend/src/srv/auth/get_info.ts' },
46
80
  { frag: 'backend/srv/auth/signin_user.ts.frag', out: 'backend/src/srv/auth/signin_user.ts' },
47
81
  { frag: 'backend/srv/auth/signout_user.ts.frag', out: 'backend/src/srv/auth/signout_user.ts' },
@@ -49,6 +83,62 @@ const WEB_FILES: { frag: string, out: string }[] = [
49
83
  { frag: 'backend/srv/auth/web_signin_user.ts.frag', out: 'backend/src/srv/auth/web_signin_user.ts' },
50
84
  { frag: 'backend/srv/auth/web_signout_user.ts.frag', out: 'backend/src/srv/auth/web_signout_user.ts' },
51
85
  { frag: 'backend/srv/auth/web_load_auth.ts.frag', out: 'backend/src/srv/auth/web_load_auth.ts' },
86
+
87
+ // Auth settings/security extensions.
88
+ { frag: 'backend/srv/auth/change_pass.ts.frag', out: 'backend/src/srv/auth/change_pass.ts' },
89
+ { frag: 'backend/srv/auth/web_change_pass.ts.frag', out: 'backend/src/srv/auth/web_change_pass.ts' },
90
+ { frag: 'backend/srv/auth/update_user.ts.frag', out: 'backend/src/srv/auth/update_user.ts' },
91
+ { frag: 'backend/srv/auth/web_update_user.ts.frag', out: 'backend/src/srv/auth/web_update_user.ts' },
92
+ { frag: 'backend/srv/auth/remind_pass.ts.frag', out: 'backend/src/srv/auth/remind_pass.ts' },
93
+ { frag: 'backend/srv/auth/web_remind_pass.ts.frag', out: 'backend/src/srv/auth/web_remind_pass.ts' },
94
+
95
+ // API access keys (Settings & security + REST API auth).
96
+ { frag: 'backend/srv/auth/apikey_util.ts.frag', out: 'backend/src/srv/auth/apikey_util.ts' },
97
+ { frag: 'backend/srv/auth/create_apikey.ts.frag', out: 'backend/src/srv/auth/create_apikey.ts' },
98
+ { frag: 'backend/srv/auth/list_apikey.ts.frag', out: 'backend/src/srv/auth/list_apikey.ts' },
99
+ { frag: 'backend/srv/auth/revoke_apikey.ts.frag', out: 'backend/src/srv/auth/revoke_apikey.ts' },
100
+ { frag: 'backend/srv/auth/web_create_apikey.ts.frag', out: 'backend/src/srv/auth/web_create_apikey.ts' },
101
+ { frag: 'backend/srv/auth/web_list_apikey.ts.frag', out: 'backend/src/srv/auth/web_list_apikey.ts' },
102
+ { frag: 'backend/srv/auth/web_revoke_apikey.ts.frag', out: 'backend/src/srv/auth/web_revoke_apikey.ts' },
103
+
104
+ // The strict-JSON REST API (model main.api): api service + HTTP mapping.
105
+ // valid_gen.ts (request validation shapes) is REGENERATED by api_gen.
106
+ { frag: 'backend/srv/api/api-srv.ts.frag', out: 'backend/src/srv/api/api-srv.ts' },
107
+ { frag: 'backend/srv/api/get_info.ts.frag', out: 'backend/src/srv/api/get_info.ts' },
108
+ { frag: 'backend/srv/api/on_ent.ts.frag', out: 'backend/src/srv/api/on_ent.ts' },
109
+ { frag: 'backend/srv/api/expose.ts.frag', out: 'backend/src/srv/api/expose.ts' },
110
+ { frag: 'backend/env/web/api.ts.frag', out: 'backend/src/env/web/api.ts' },
111
+
112
+ // Unit tests for what EnvWeb generates (node:test). They are
113
+ // MODEL-DRIVEN - the entity under test and its payloads come from the
114
+ // model - so they hold for any entity graph, and they stand in for
115
+ // other services with mock messages rather than booting them.
116
+ { frag: 'backend/test/unit/srv/api/api.setup.ts.frag', out: 'backend/test/unit/srv/api/api.setup.ts' },
117
+ { frag: 'backend/test/unit/srv/api/api.test.ts.frag', out: 'backend/test/unit/srv/api/api.test.ts' },
118
+ { frag: 'backend/test/unit/srv/auth/auth.setup.ts.frag', out: 'backend/test/unit/srv/auth/auth.setup.ts' },
119
+ { frag: 'backend/test/unit/srv/auth/apikey.test.ts.frag', out: 'backend/test/unit/srv/auth/apikey.test.ts' },
120
+ { frag: 'backend/test/unit/srv/auth/session.test.ts.frag', out: 'backend/test/unit/srv/auth/session.test.ts' },
121
+ { frag: 'backend/test/unit/env/web/api-router.test.ts.frag', out: 'backend/test/unit/env/web/api-router.test.ts' },
122
+ { frag: 'backend/test/unit/env/web/surface.test.ts.frag', out: 'backend/test/unit/env/web/surface.test.ts' },
123
+ { frag: 'backend/test/unit/srv/ent/ent.setup.ts.frag', out: 'backend/test/unit/srv/ent/ent.setup.ts' },
124
+ { frag: 'backend/test/unit/srv/ent/proxy.test.ts.frag', out: 'backend/test/unit/srv/ent/proxy.test.ts' },
125
+
126
+ // Generic entity service (parameterised CRUD, membership-scoped).
127
+ { frag: 'backend/srv/ent/ent-srv.ts.frag', out: 'backend/src/srv/ent/ent-srv.ts' },
128
+ { frag: 'backend/srv/ent/access.ts.frag', out: 'backend/src/srv/ent/access.ts' },
129
+ { frag: 'backend/srv/ent/get_info.ts.frag', out: 'backend/src/srv/ent/get_info.ts' },
130
+ { frag: 'backend/srv/ent/cmd_list.ts.frag', out: 'backend/src/srv/ent/cmd_list.ts' },
131
+ { frag: 'backend/srv/ent/cmd_load.ts.frag', out: 'backend/src/srv/ent/cmd_load.ts' },
132
+ { frag: 'backend/srv/ent/cmd_save.ts.frag', out: 'backend/src/srv/ent/cmd_save.ts' },
133
+ { frag: 'backend/srv/ent/cmd_remove.ts.frag', out: 'backend/src/srv/ent/cmd_remove.ts' },
134
+
135
+ // Browser proxies: aim:web,on:ent,cmd:* -> aim:ent,cmd:* (the gateway
136
+ // accepts only aim:web, so the entity service is never posted directly
137
+ // from a browser).
138
+ { frag: 'backend/srv/ent/web_cmd_list.ts.frag', out: 'backend/src/srv/ent/web_cmd_list.ts' },
139
+ { frag: 'backend/srv/ent/web_cmd_load.ts.frag', out: 'backend/src/srv/ent/web_cmd_load.ts' },
140
+ { frag: 'backend/srv/ent/web_cmd_save.ts.frag', out: 'backend/src/srv/ent/web_cmd_save.ts' },
141
+ { frag: 'backend/srv/ent/web_cmd_remove.ts.frag', out: 'backend/src/srv/ent/web_cmd_remove.ts' },
52
142
  ]
53
143
 
54
144
 
@@ -59,19 +149,51 @@ const DEFAULT_USERS = [
59
149
  ]
60
150
 
61
151
 
152
+ // The frontend folder name, from the model env entry (`web: { dir: ... }`).
153
+ // Defaults to 'web', so a project that says nothing keeps the layout it
154
+ // already has. Everything that names the folder - the WEB_FILES output
155
+ // paths, views.js, theme.css, and the static path the backend runner
156
+ // serves - goes through this one value, via the $$fe$$ slot.
157
+ //
158
+ // Only the FOLDER moves. The `aim:web` message namespace is unrelated and
159
+ // must never follow it: the gateway allow-list is the literal
160
+ // { 'aim:web': true }, and backend/src/env/web/ is the backend runner, not
161
+ // the SPA.
162
+ const DEFAULT_FE = 'web'
163
+
164
+ // Absent means 'web'. ANYTHING ELSE provided is validated rather than
165
+ // silently defaulted - `dir: ''` is a mistake, not a request for 'web', and
166
+ // defaulting it would generate the whole SPA into the project root.
167
+ function feDir(envdef: any): string {
168
+ const dir = null == envdef ? undefined : envdef.dir
169
+ if (null == dir) {
170
+ return DEFAULT_FE
171
+ }
172
+ if ('string' !== typeof dir || '' === dir || dir.split('/').includes('..') ||
173
+ Path.isAbsolute(dir)) {
174
+ throw new Error(
175
+ 'EnvWeb: env web `dir` must be a non-empty relative folder name, got: ' +
176
+ JSON.stringify(dir))
177
+ }
178
+ return dir
179
+ }
180
+
181
+
62
182
  // web_gen(model, spec): generate the frontend + backend web pieces into
63
183
  // the project. spec.root is the project root (parent of backend/); files
64
184
  // that already exist are left untouched (create-once).
65
185
  const web_gen = async (model: any, spec: {
66
- root: string // project root (holds backend/ and web/)
186
+ root: string // project root (holds backend/ and the frontend folder)
67
187
  tm?: string // project tm/web folder (fragment shadowing)
68
- env?: any // the model env entry (config: port, users)
188
+ env?: any // the model env entry (config: port, users, dir)
69
189
  force?: boolean // overwrite existing files (default: create-once)
70
190
  }) => {
71
191
 
72
192
  const core = CoreConfShape(model.main.conf.core)
73
193
  const envdef = spec.env || {}
74
194
 
195
+ const fe = feDir(envdef)
196
+
75
197
  const users = envdef.users || DEFAULT_USERS
76
198
  const seed = users[0] || DEFAULT_USERS[0]
77
199
  const port = envdef.port ||
@@ -80,6 +202,7 @@ const web_gen = async (model: any, spec: {
80
202
  const slots: Record<string, any> = {
81
203
  name: core.name,
82
204
  Name: camelify(core.name),
205
+ fe,
83
206
  users: JSON.stringify(users, null, 2),
84
207
  e2eport: envdef.e2eport || (port + 10),
85
208
  seedEmail: seed.email,
@@ -104,6 +227,89 @@ const web_gen = async (model: any, spec: {
104
227
  created.push(outrel)
105
228
  }
106
229
 
230
+ // Custom entity views: for every entity declaring `ux:{ view: 'custom' }`,
231
+ // generate a hand-coded view component (CREATE-ONCE, so edits survive) plus
232
+ // an always-regenerated index (views.js) that imports them.
233
+ const entzones = model.main.ent || {}
234
+ const customs: { zone: string, name: string, canon: string }[] = []
235
+ for (const zone of Object.keys(entzones)) {
236
+ for (const name of Object.keys(entzones[zone])) {
237
+ const def = entzones[zone][name]
238
+ if (def && def.ux && 'custom' === def.ux.view) {
239
+ customs.push({ zone, name, canon: zone + '/' + name })
240
+ }
241
+ }
242
+ }
243
+
244
+ for (const c of customs) {
245
+ const vslots = Object.assign({}, slots, {
246
+ canon: c.canon,
247
+ zone: c.zone,
248
+ name: c.name,
249
+ tag: 'vg-view-' + c.zone + '-' + c.name,
250
+ className: 'VgView' + camelify(c.zone) + camelify(c.name),
251
+ Label: camelify(c.name),
252
+ })
253
+ // The component starter plus its doc sidecar, both create-once.
254
+ for (const ext of ['js', 'md']) {
255
+ const outrel = fe + '/src/cmp/view/' + c.zone + '_' + c.name + '.' + ext
256
+ const dest = Path.join(spec.root, outrel)
257
+ if (Fs.existsSync(dest) && !spec.force) {
258
+ skipped.push(outrel)
259
+ continue
260
+ }
261
+ const content = renderFragment(
262
+ loadFragment('web/src/cmp/view/custom-view.' + ext + '.frag', spec, 'web'), vslots)
263
+ Fs.mkdirSync(Path.dirname(dest), { recursive: true })
264
+ Fs.writeFileSync(dest, content)
265
+ created.push(outrel)
266
+ }
267
+ }
268
+
269
+ // views.js — the generated index of custom views. Regenerated whenever the
270
+ // set of custom views changes; a no-op run leaves it (and `created`) alone.
271
+ const viewsRel = fe + '/src/views.js'
272
+ const viewsPath = Path.join(spec.root, viewsRel)
273
+ const viewsBody =
274
+ '// AUTO-GENERATED: imports every custom entity view (model entities declaring\n' +
275
+ '// ux:{view:\'custom\'}). Regenerated from the model — do not edit. The view\n' +
276
+ '// files it imports are hand-coded and create-once.\n\n' +
277
+ customs.map((c) => `import './cmp/view/${c.zone}_${c.name}.js'`).join('\n') + '\n'
278
+ const existingViews = Fs.existsSync(viewsPath) ? Fs.readFileSync(viewsPath, 'utf8') : null
279
+ if (existingViews !== viewsBody) {
280
+ Fs.mkdirSync(Path.join(spec.root, fe, 'src'), { recursive: true })
281
+ Fs.writeFileSync(viewsPath, viewsBody)
282
+ created.push(viewsRel)
283
+ }
284
+
285
+ // theme.css — the design theme from the model (main.theme). Each mode's
286
+ // tokens become CSS variables under :root[data-theme-mode="<mode>"], with the
287
+ // default mode also on plain :root. Regenerated from the model; override
288
+ // tokens in custom.css and add modes via the theme:modes hook.
289
+ const theme = model.main && model.main.theme
290
+ if (theme && theme.modes && Object.keys(theme.modes).length) {
291
+ const defMode = theme.mode || Object.keys(theme.modes)[0]
292
+ const tokensCss = (tokens: any) => Object.keys(tokens).sort()
293
+ .map((k) => ` --vg-${k}: ${tokens[k]};`).join('\n')
294
+ const blocks = [':root,\n:root[data-theme-mode="' + defMode + '"] {\n' + tokensCss(theme.modes[defMode]) + '\n}']
295
+ for (const mode of Object.keys(theme.modes)) {
296
+ if (mode !== defMode) {
297
+ blocks.push(':root[data-theme-mode="' + mode + '"] {\n' + tokensCss(theme.modes[mode]) + '\n}')
298
+ }
299
+ }
300
+ const themeBody =
301
+ '/* AUTO-GENERATED from the model theme (main.theme) — do not edit.\n' +
302
+ ' Override tokens in custom.css; add modes via the theme:modes hook. */\n\n' +
303
+ blocks.join('\n\n') + '\n'
304
+ const themePath = Path.join(spec.root, fe, 'src/theme.css')
305
+ const existingTheme = Fs.existsSync(themePath) ? Fs.readFileSync(themePath, 'utf8') : null
306
+ if (existingTheme !== themeBody) {
307
+ Fs.mkdirSync(Path.join(spec.root, fe, 'src'), { recursive: true })
308
+ Fs.writeFileSync(themePath, themeBody)
309
+ created.push(fe + '/src/theme.css')
310
+ }
311
+ }
312
+
107
313
  return { created: created.sort(), skipped: skipped.sort() }
108
314
  }
109
315