@carlonicora/nextjs-jsonapi 1.2.0 → 1.3.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 (202) hide show
  1. package/dist/scripts/generate-web-module/generator.d.ts +15 -0
  2. package/dist/scripts/generate-web-module/generator.d.ts.map +1 -0
  3. package/dist/scripts/generate-web-module/generator.js +320 -0
  4. package/dist/scripts/generate-web-module/generator.js.map +1 -0
  5. package/dist/scripts/generate-web-module/index.d.ts +16 -0
  6. package/dist/scripts/generate-web-module/index.d.ts.map +1 -0
  7. package/dist/scripts/generate-web-module/index.js +80 -0
  8. package/dist/scripts/generate-web-module/index.js.map +1 -0
  9. package/dist/scripts/generate-web-module/templates/components/container.template.d.ts +14 -0
  10. package/dist/scripts/generate-web-module/templates/components/container.template.d.ts.map +1 -0
  11. package/dist/scripts/generate-web-module/templates/components/container.template.js +124 -0
  12. package/dist/scripts/generate-web-module/templates/components/container.template.js.map +1 -0
  13. package/dist/scripts/generate-web-module/templates/components/content.template.d.ts +15 -0
  14. package/dist/scripts/generate-web-module/templates/components/content.template.d.ts.map +1 -0
  15. package/dist/scripts/generate-web-module/templates/components/content.template.js +39 -0
  16. package/dist/scripts/generate-web-module/templates/components/content.template.js.map +1 -0
  17. package/dist/scripts/generate-web-module/templates/components/deleter.template.d.ts +14 -0
  18. package/dist/scripts/generate-web-module/templates/components/deleter.template.d.ts.map +1 -0
  19. package/dist/scripts/generate-web-module/templates/components/deleter.template.js +51 -0
  20. package/dist/scripts/generate-web-module/templates/components/deleter.template.js.map +1 -0
  21. package/dist/scripts/generate-web-module/templates/components/details.template.d.ts +14 -0
  22. package/dist/scripts/generate-web-module/templates/components/details.template.d.ts.map +1 -0
  23. package/dist/scripts/generate-web-module/templates/components/details.template.js +109 -0
  24. package/dist/scripts/generate-web-module/templates/components/details.template.js.map +1 -0
  25. package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts +14 -0
  26. package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts.map +1 -0
  27. package/dist/scripts/generate-web-module/templates/components/editor.template.js +459 -0
  28. package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -0
  29. package/dist/scripts/generate-web-module/templates/components/index.d.ts +15 -0
  30. package/dist/scripts/generate-web-module/templates/components/index.d.ts.map +1 -0
  31. package/dist/scripts/generate-web-module/templates/components/index.js +29 -0
  32. package/dist/scripts/generate-web-module/templates/components/index.js.map +1 -0
  33. package/dist/scripts/generate-web-module/templates/components/list-container.template.d.ts +14 -0
  34. package/dist/scripts/generate-web-module/templates/components/list-container.template.d.ts.map +1 -0
  35. package/dist/scripts/generate-web-module/templates/components/list-container.template.js +30 -0
  36. package/dist/scripts/generate-web-module/templates/components/list-container.template.js.map +1 -0
  37. package/dist/scripts/generate-web-module/templates/components/list.template.d.ts +14 -0
  38. package/dist/scripts/generate-web-module/templates/components/list.template.d.ts.map +1 -0
  39. package/dist/scripts/generate-web-module/templates/components/list.template.js +84 -0
  40. package/dist/scripts/generate-web-module/templates/components/list.template.js.map +1 -0
  41. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts +14 -0
  42. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -0
  43. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +184 -0
  44. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -0
  45. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts +14 -0
  46. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -0
  47. package/dist/scripts/generate-web-module/templates/components/selector.template.js +199 -0
  48. package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -0
  49. package/dist/scripts/generate-web-module/templates/context.template.d.ts +14 -0
  50. package/dist/scripts/generate-web-module/templates/context.template.d.ts.map +1 -0
  51. package/dist/scripts/generate-web-module/templates/context.template.js +121 -0
  52. package/dist/scripts/generate-web-module/templates/context.template.js.map +1 -0
  53. package/dist/scripts/generate-web-module/templates/data/fields.template.d.ts +14 -0
  54. package/dist/scripts/generate-web-module/templates/data/fields.template.d.ts.map +1 -0
  55. package/dist/scripts/generate-web-module/templates/data/fields.template.js +55 -0
  56. package/dist/scripts/generate-web-module/templates/data/fields.template.js.map +1 -0
  57. package/dist/scripts/generate-web-module/templates/data/index.d.ts +10 -0
  58. package/dist/scripts/generate-web-module/templates/data/index.d.ts.map +1 -0
  59. package/dist/scripts/generate-web-module/templates/data/index.js +26 -0
  60. package/dist/scripts/generate-web-module/templates/data/index.js.map +1 -0
  61. package/dist/scripts/generate-web-module/templates/data/interface.template.d.ts +14 -0
  62. package/dist/scripts/generate-web-module/templates/data/interface.template.d.ts.map +1 -0
  63. package/dist/scripts/generate-web-module/templates/data/interface.template.js +116 -0
  64. package/dist/scripts/generate-web-module/templates/data/interface.template.js.map +1 -0
  65. package/dist/scripts/generate-web-module/templates/data/model.template.d.ts +14 -0
  66. package/dist/scripts/generate-web-module/templates/data/model.template.d.ts.map +1 -0
  67. package/dist/scripts/generate-web-module/templates/data/model.template.js +274 -0
  68. package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -0
  69. package/dist/scripts/generate-web-module/templates/data/service.template.d.ts +14 -0
  70. package/dist/scripts/generate-web-module/templates/data/service.template.d.ts.map +1 -0
  71. package/dist/scripts/generate-web-module/templates/data/service.template.js +168 -0
  72. package/dist/scripts/generate-web-module/templates/data/service.template.js.map +1 -0
  73. package/dist/scripts/generate-web-module/templates/index.d.ts +12 -0
  74. package/dist/scripts/generate-web-module/templates/index.d.ts.map +1 -0
  75. package/dist/scripts/generate-web-module/templates/index.js +37 -0
  76. package/dist/scripts/generate-web-module/templates/index.js.map +1 -0
  77. package/dist/scripts/generate-web-module/templates/module.template.d.ts +14 -0
  78. package/dist/scripts/generate-web-module/templates/module.template.d.ts.map +1 -0
  79. package/dist/scripts/generate-web-module/templates/module.template.js +64 -0
  80. package/dist/scripts/generate-web-module/templates/module.template.js.map +1 -0
  81. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.d.ts +14 -0
  82. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.d.ts.map +1 -0
  83. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js +65 -0
  84. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js.map +1 -0
  85. package/dist/scripts/generate-web-module/templates/pages/index.d.ts +8 -0
  86. package/dist/scripts/generate-web-module/templates/pages/index.d.ts.map +1 -0
  87. package/dist/scripts/generate-web-module/templates/pages/index.js +13 -0
  88. package/dist/scripts/generate-web-module/templates/pages/index.js.map +1 -0
  89. package/dist/scripts/generate-web-module/templates/pages/list-page.template.d.ts +14 -0
  90. package/dist/scripts/generate-web-module/templates/pages/list-page.template.d.ts.map +1 -0
  91. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +37 -0
  92. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -0
  93. package/dist/scripts/generate-web-module/templates/table-hook.template.d.ts +14 -0
  94. package/dist/scripts/generate-web-module/templates/table-hook.template.d.ts.map +1 -0
  95. package/dist/scripts/generate-web-module/templates/table-hook.template.js +174 -0
  96. package/dist/scripts/generate-web-module/templates/table-hook.template.js.map +1 -0
  97. package/dist/scripts/generate-web-module/transformers/field-mapper.d.ts +55 -0
  98. package/dist/scripts/generate-web-module/transformers/field-mapper.d.ts.map +1 -0
  99. package/dist/scripts/generate-web-module/transformers/field-mapper.js +179 -0
  100. package/dist/scripts/generate-web-module/transformers/field-mapper.js.map +1 -0
  101. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts +78 -0
  102. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -0
  103. package/dist/scripts/generate-web-module/transformers/i18n-generator.js +182 -0
  104. package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -0
  105. package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts +106 -0
  106. package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts.map +1 -0
  107. package/dist/scripts/generate-web-module/transformers/import-resolver.js +193 -0
  108. package/dist/scripts/generate-web-module/transformers/import-resolver.js.map +1 -0
  109. package/dist/scripts/generate-web-module/transformers/index.d.ts +12 -0
  110. package/dist/scripts/generate-web-module/transformers/index.d.ts.map +1 -0
  111. package/dist/scripts/generate-web-module/transformers/index.js +28 -0
  112. package/dist/scripts/generate-web-module/transformers/index.js.map +1 -0
  113. package/dist/scripts/generate-web-module/transformers/name-transformer.d.ts +60 -0
  114. package/dist/scripts/generate-web-module/transformers/name-transformer.d.ts.map +1 -0
  115. package/dist/scripts/generate-web-module/transformers/name-transformer.js +115 -0
  116. package/dist/scripts/generate-web-module/transformers/name-transformer.js.map +1 -0
  117. package/dist/scripts/generate-web-module/transformers/parent-detector.d.ts +57 -0
  118. package/dist/scripts/generate-web-module/transformers/parent-detector.d.ts.map +1 -0
  119. package/dist/scripts/generate-web-module/transformers/parent-detector.js +88 -0
  120. package/dist/scripts/generate-web-module/transformers/parent-detector.js.map +1 -0
  121. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts +68 -0
  122. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -0
  123. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +219 -0
  124. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -0
  125. package/dist/scripts/generate-web-module/types/field-mapping.types.d.ts +68 -0
  126. package/dist/scripts/generate-web-module/types/field-mapping.types.d.ts.map +1 -0
  127. package/dist/scripts/generate-web-module/types/field-mapping.types.js +129 -0
  128. package/dist/scripts/generate-web-module/types/field-mapping.types.js.map +1 -0
  129. package/dist/scripts/generate-web-module/types/index.d.ts +9 -0
  130. package/dist/scripts/generate-web-module/types/index.d.ts.map +1 -0
  131. package/dist/scripts/generate-web-module/types/index.js +25 -0
  132. package/dist/scripts/generate-web-module/types/index.js.map +1 -0
  133. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts +40 -0
  134. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts.map +1 -0
  135. package/dist/scripts/generate-web-module/types/json-schema.interface.js +10 -0
  136. package/dist/scripts/generate-web-module/types/json-schema.interface.js.map +1 -0
  137. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +128 -0
  138. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -0
  139. package/dist/scripts/generate-web-module/types/template-data.interface.js +9 -0
  140. package/dist/scripts/generate-web-module/types/template-data.interface.js.map +1 -0
  141. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.d.ts +29 -0
  142. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.d.ts.map +1 -0
  143. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.js +153 -0
  144. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.js.map +1 -0
  145. package/dist/scripts/generate-web-module/utils/file-writer.d.ts +38 -0
  146. package/dist/scripts/generate-web-module/utils/file-writer.d.ts.map +1 -0
  147. package/dist/scripts/generate-web-module/utils/file-writer.js +126 -0
  148. package/dist/scripts/generate-web-module/utils/file-writer.js.map +1 -0
  149. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts +28 -0
  150. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -0
  151. package/dist/scripts/generate-web-module/utils/i18n-updater.js +122 -0
  152. package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -0
  153. package/dist/scripts/generate-web-module/utils/index.d.ts +9 -0
  154. package/dist/scripts/generate-web-module/utils/index.d.ts.map +1 -0
  155. package/dist/scripts/generate-web-module/utils/index.js +20 -0
  156. package/dist/scripts/generate-web-module/utils/index.js.map +1 -0
  157. package/dist/scripts/generate-web-module/validators/json-schema-validator.d.ts +46 -0
  158. package/dist/scripts/generate-web-module/validators/json-schema-validator.d.ts.map +1 -0
  159. package/dist/scripts/generate-web-module/validators/json-schema-validator.js +265 -0
  160. package/dist/scripts/generate-web-module/validators/json-schema-validator.js.map +1 -0
  161. package/package.json +27 -21
  162. package/scripts/generate-web-module/generator.ts +363 -0
  163. package/scripts/generate-web-module/index.ts +49 -0
  164. package/scripts/generate-web-module/templates/components/container.template.ts +129 -0
  165. package/scripts/generate-web-module/templates/components/content.template.ts +40 -0
  166. package/scripts/generate-web-module/templates/components/deleter.template.ts +51 -0
  167. package/scripts/generate-web-module/templates/components/details.template.ts +115 -0
  168. package/scripts/generate-web-module/templates/components/editor.template.ts +495 -0
  169. package/scripts/generate-web-module/templates/components/index.ts +18 -0
  170. package/scripts/generate-web-module/templates/components/list-container.template.ts +30 -0
  171. package/scripts/generate-web-module/templates/components/list.template.ts +91 -0
  172. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +184 -0
  173. package/scripts/generate-web-module/templates/components/selector.template.ts +199 -0
  174. package/scripts/generate-web-module/templates/context.template.ts +121 -0
  175. package/scripts/generate-web-module/templates/data/fields.template.ts +64 -0
  176. package/scripts/generate-web-module/templates/data/index.ts +10 -0
  177. package/scripts/generate-web-module/templates/data/interface.template.ts +136 -0
  178. package/scripts/generate-web-module/templates/data/model.template.ts +327 -0
  179. package/scripts/generate-web-module/templates/data/service.template.ts +185 -0
  180. package/scripts/generate-web-module/templates/index.ts +34 -0
  181. package/scripts/generate-web-module/templates/module.template.ts +69 -0
  182. package/scripts/generate-web-module/templates/pages/detail-page.template.ts +65 -0
  183. package/scripts/generate-web-module/templates/pages/index.ts +8 -0
  184. package/scripts/generate-web-module/templates/pages/list-page.template.ts +37 -0
  185. package/scripts/generate-web-module/templates/table-hook.template.ts +182 -0
  186. package/scripts/generate-web-module/transformers/field-mapper.ts +201 -0
  187. package/scripts/generate-web-module/transformers/i18n-generator.ts +199 -0
  188. package/scripts/generate-web-module/transformers/import-resolver.ts +250 -0
  189. package/scripts/generate-web-module/transformers/index.ts +12 -0
  190. package/scripts/generate-web-module/transformers/name-transformer.ts +115 -0
  191. package/scripts/generate-web-module/transformers/parent-detector.ts +87 -0
  192. package/scripts/generate-web-module/transformers/relationship-resolver.ts +221 -0
  193. package/scripts/generate-web-module/tsconfig.json +24 -0
  194. package/scripts/generate-web-module/types/field-mapping.types.ts +141 -0
  195. package/scripts/generate-web-module/types/index.ts +9 -0
  196. package/scripts/generate-web-module/types/json-schema.interface.ts +42 -0
  197. package/scripts/generate-web-module/types/template-data.interface.ts +164 -0
  198. package/scripts/generate-web-module/utils/bootstrapper-updater.ts +145 -0
  199. package/scripts/generate-web-module/utils/file-writer.ts +115 -0
  200. package/scripts/generate-web-module/utils/i18n-updater.ts +108 -0
  201. package/scripts/generate-web-module/utils/index.ts +9 -0
  202. package/scripts/generate-web-module/validators/json-schema-validator.ts +306 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -8,6 +8,9 @@
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.mjs",
10
10
  "types": "./dist/index.d.ts",
11
+ "bin": {
12
+ "generate-web-module": "./dist/scripts/generate-web-module/index.js"
13
+ },
11
14
  "exports": {
12
15
  ".": {
13
16
  "types": "./dist/index.d.ts",
@@ -81,16 +84,18 @@
81
84
  }
82
85
  },
83
86
  "scripts": {
84
- "build": "tsup",
87
+ "build": "tsup && tsc -p scripts/generate-web-module/tsconfig.json",
85
88
  "dev": "tsup --watch",
86
89
  "clean": "rm -rf dist",
87
90
  "lint": "eslint \"src/**/*.ts\" --fix",
88
91
  "format": "prettier --write \"src/**/*.ts\"",
89
- "prepublishOnly": "pnpm build"
92
+ "prepublishOnly": "pnpm build",
93
+ "generate-web-module": "node dist/scripts/generate-web-module/index.js"
90
94
  },
91
95
  "files": [
92
96
  "dist",
93
97
  "src",
98
+ "scripts",
94
99
  "README.md"
95
100
  ],
96
101
  "peerDependencies": {
@@ -113,9 +118,10 @@
113
118
  }
114
119
  },
115
120
  "dependencies": {
116
- "@blocknote/core": "^0.44.0",
117
- "@blocknote/react": "^0.44.0",
118
- "@blocknote/shadcn": "^0.44.0",
121
+ "commander": "^13.1.0",
122
+ "@blocknote/core": "^0.44.2",
123
+ "@blocknote/react": "^0.44.2",
124
+ "@blocknote/shadcn": "^0.44.2",
119
125
  "@dnd-kit/core": "^6.3.1",
120
126
  "@dnd-kit/sortable": "^10.0.0",
121
127
  "@dnd-kit/utilities": "^3.2.2",
@@ -151,10 +157,10 @@
151
157
  "d3": "^7.9.0",
152
158
  "date-fns": "^4.1.0",
153
159
  "embla-carousel-react": "^8.6.0",
154
- "jotai": "^2.15.2",
155
- "lucide-react": "^0.556.0",
160
+ "jotai": "^2.16.0",
161
+ "lucide-react": "^0.561.0",
156
162
  "next-themes": "^0.4.6",
157
- "react-day-picker": "^9.11.3",
163
+ "react-day-picker": "^9.12.0",
158
164
  "react-dropzone": "^14.3.8",
159
165
  "react-hook-form": "^7.68.0",
160
166
  "react-markdown": "^10.1.0",
@@ -165,7 +171,7 @@
165
171
  "sonner": "^2.0.7",
166
172
  "tailwind-merge": "^3.4.0",
167
173
  "uuid": "^13.0.0",
168
- "vaul": "^1.1.0",
174
+ "vaul": "^1.1.2",
169
175
  "zod": "^4.1.13"
170
176
  },
171
177
  "devDependencies": {
@@ -177,22 +183,22 @@
177
183
  "@eslint/js": "^9.39.1",
178
184
  "@tanstack/react-table": "^8.21.3",
179
185
  "@types/d3": "^7.4.3",
180
- "@types/node": "^22.0.0",
181
- "@types/react": "^19.0.0",
182
- "@types/react-dom": "^19.0.0",
183
- "@typescript-eslint/eslint-plugin": "^8.48.1",
184
- "@typescript-eslint/parser": "^8.48.1",
186
+ "@types/node": "^25.0.1",
187
+ "@types/react": "^19.2.7",
188
+ "@types/react-dom": "^19.2.3",
189
+ "@typescript-eslint/eslint-plugin": "^8.49.0",
190
+ "@typescript-eslint/parser": "^8.49.0",
185
191
  "eslint": "^9.39.1",
186
192
  "eslint-config-prettier": "^10.1.8",
187
193
  "eslint-plugin-prettier": "^5.5.4",
188
194
  "globals": "^16.5.0",
189
- "next": "^16.0.0",
190
- "next-intl": "^4.1.0",
195
+ "next": "^16.0.10",
196
+ "next-intl": "^4.5.8",
191
197
  "prettier": "^3.7.4",
192
- "react": "^19.0.0",
193
- "react-dom": "^19.0.0",
194
- "tsup": "^8.5.0",
195
- "typescript": "^5.9.0"
198
+ "react": "^19.2.3",
199
+ "react-dom": "^19.2.3",
200
+ "tsup": "^8.5.1",
201
+ "typescript": "^5.9.3"
196
202
  },
197
203
  "keywords": [
198
204
  "nextjs",
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Frontend Module Generator
3
+ *
4
+ * Main orchestrator for generating frontend modules from JSON schema.
5
+ */
6
+
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import {
10
+ JsonModuleDefinition,
11
+ FrontendTemplateData,
12
+ GeneratedFile,
13
+ GenerateWebModuleOptions,
14
+ } from "./types";
15
+ import { transformNames, toCamelCase, pluralize, toTitleCase } from "./transformers/name-transformer";
16
+ import { detectExtendsContent } from "./transformers/parent-detector";
17
+ import { mapFields, filterInheritedFields } from "./transformers/field-mapper";
18
+ import { resolveRelationships, generateServiceMethods } from "./transformers/relationship-resolver";
19
+ import { buildImportStatements, buildFilePaths } from "./transformers/import-resolver";
20
+ import { generateI18nKeys } from "./transformers/i18n-generator";
21
+ import { parseAndValidate, validationPassed } from "./validators/json-schema-validator";
22
+ import {
23
+ generateInterfaceTemplate,
24
+ generateModelTemplate,
25
+ generateServiceTemplate,
26
+ generateFieldsTemplate,
27
+ generateEditorTemplate,
28
+ generateDeleterTemplate,
29
+ generateSelectorTemplate,
30
+ generateMultiSelectorTemplate,
31
+ generateListTemplate,
32
+ generateDetailsTemplate,
33
+ generateContentTemplate,
34
+ generateContainerTemplate,
35
+ generateListContainerTemplate,
36
+ generateContextTemplate,
37
+ generateTableHookTemplate,
38
+ generateModuleTemplate,
39
+ generateListPageTemplate,
40
+ generateDetailPageTemplate,
41
+ } from "./templates";
42
+ import { writeFiles, printResults, updateBootstrapper, updateI18n } from "./utils";
43
+
44
+ /**
45
+ * Generate all frontend module files from JSON schema
46
+ *
47
+ * @param options - Generator options
48
+ * @returns True if generation succeeded
49
+ */
50
+ export async function generateWebModule(options: GenerateWebModuleOptions): Promise<boolean> {
51
+ const { jsonPath, dryRun = false, force = false, noRegister = false } = options;
52
+
53
+ console.log(`\n🔧 Frontend Module Generator`);
54
+ console.log(` JSON Schema: ${jsonPath}`);
55
+ console.log(` Dry Run: ${dryRun}`);
56
+ console.log(` Force: ${force}`);
57
+ console.log(` No Register: ${noRegister}`);
58
+
59
+ // Step 1: Validate JSON schema
60
+ console.log("\n📋 Validating JSON schema...");
61
+ const validation = parseAndValidate(jsonPath);
62
+
63
+ if (!validationPassed(validation)) {
64
+ console.error("\n❌ Validation failed:");
65
+ validation.errors.forEach((e) => console.error(` - ${e}`));
66
+ return false;
67
+ }
68
+
69
+ if (validation.warnings.length > 0) {
70
+ console.log("\n⚠️ Validation warnings (non-blocking):");
71
+ validation.warnings.forEach((w) => console.log(` - ${w}`));
72
+ }
73
+
74
+ const schema = validation.data!;
75
+ console.log(` ✅ Schema valid: ${schema.moduleName}`);
76
+
77
+ // Step 2: Build template data
78
+ console.log("\n🔨 Building template data...");
79
+ const templateData = buildTemplateData(schema);
80
+ console.log(` Module: ${templateData.names.pascalCase}`);
81
+ console.log(` Target: ${templateData.targetDir}`);
82
+ console.log(` Extends Content: ${templateData.extendsContent}`);
83
+ console.log(` Fields: ${templateData.fields.length}`);
84
+ console.log(` Relationships: ${templateData.relationships.length}`);
85
+
86
+ // Step 3: Generate all files
87
+ console.log("\n📝 Generating files...");
88
+ const files = generateAllFiles(templateData, schema);
89
+ console.log(` Generated ${files.length} file templates`);
90
+
91
+ // Step 4: Determine web base path
92
+ const webBasePath = findWebBasePath(jsonPath);
93
+ if (!webBasePath) {
94
+ console.error("\n❌ Could not determine web app base path");
95
+ return false;
96
+ }
97
+ console.log(` Web base path: ${webBasePath}`);
98
+
99
+ // Step 5: Write files
100
+ console.log("\n💾 Writing files...");
101
+ const results = writeFiles(files, { dryRun, force });
102
+ printResults(results);
103
+
104
+ // Step 6: Update Bootstrapper (unless --no-register)
105
+ if (!noRegister) {
106
+ console.log("\n🔧 Updating Bootstrapper.ts...");
107
+ const bootstrapResult = updateBootstrapper(templateData, webBasePath, dryRun);
108
+ console.log(` ${bootstrapResult.success ? "✅" : "❌"} ${bootstrapResult.message}`);
109
+ }
110
+
111
+ // Step 7: Update i18n (unless --no-register)
112
+ if (!noRegister) {
113
+ console.log("\n🌐 Updating messages/en.json...");
114
+ const i18nResult = updateI18n(templateData, webBasePath, dryRun);
115
+ console.log(` ${i18nResult.success ? "✅" : "❌"} ${i18nResult.message}`);
116
+ }
117
+
118
+ console.log("\n✅ Generation complete!");
119
+
120
+ if (dryRun) {
121
+ console.log("\n📝 This was a dry run. No files were actually written.");
122
+ console.log(" Remove --dry-run to generate files.");
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Build template data from JSON schema
130
+ */
131
+ function buildTemplateData(schema: JsonModuleDefinition): FrontendTemplateData {
132
+ const names = transformNames(schema.moduleName, schema.endpointName);
133
+ const targetDir = schema.targetDir as "features" | "foundations";
134
+ const extendsContent = detectExtendsContent(schema.fields);
135
+
136
+ // Map fields
137
+ const allFields = mapFields(schema.fields, names.camelCase);
138
+
139
+ // Get inherited fields to filter out for forms (Content fields are handled specially)
140
+ const inheritedFields = extendsContent ? ["name", "tldr", "abstract", "content"] : [];
141
+ const fields = filterInheritedFields(allFields, inheritedFields);
142
+
143
+ // Resolve relationships
144
+ const relationships = resolveRelationships(schema.relationships);
145
+ const relationshipServiceMethods = generateServiceMethods(relationships);
146
+
147
+ // Build imports
148
+ const imports = buildImportStatements(relationships, extendsContent);
149
+
150
+ // Generate i18n keys
151
+ const i18nKeys = generateI18nKeys(names, fields, relationships);
152
+
153
+ // Build table field names
154
+ const tableFieldNames = buildTableFieldNames(schema, extendsContent);
155
+
156
+ return {
157
+ names,
158
+ moduleId: schema.moduleId,
159
+ endpoint: schema.endpointName,
160
+ targetDir,
161
+ extendsContent,
162
+ fields: allFields, // Keep all fields for interface generation
163
+ relationships,
164
+ i18nKeys,
165
+ imports,
166
+ tableFieldNames,
167
+ relationshipServiceMethods,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Build field names for table display
173
+ */
174
+ function buildTableFieldNames(schema: JsonModuleDefinition, extendsContent: boolean): string[] {
175
+ const fieldNames: string[] = [`${toCamelCase(schema.moduleName)}Id`];
176
+
177
+ // Add name for all modules
178
+ fieldNames.push("name");
179
+
180
+ // Add authors for content modules
181
+ if (extendsContent) {
182
+ fieldNames.push("authors");
183
+ }
184
+
185
+ // Add custom fields (excluding certain ones)
186
+ schema.fields.forEach((f) => {
187
+ if (!["id", "name", "tldr", "abstract", "content", "createdAt", "updatedAt"].includes(f.name)) {
188
+ fieldNames.push(f.name);
189
+ }
190
+ });
191
+
192
+ // Add standard date fields
193
+ fieldNames.push("createdAt", "updatedAt");
194
+
195
+ return fieldNames;
196
+ }
197
+
198
+ /**
199
+ * Generate all module files
200
+ */
201
+ function generateAllFiles(data: FrontendTemplateData, schema: JsonModuleDefinition): GeneratedFile[] {
202
+ const files: GeneratedFile[] = [];
203
+ const paths = buildFilePaths(data, findWebBasePath(""));
204
+
205
+ // Data layer (4 files)
206
+ files.push({
207
+ path: paths.interface,
208
+ content: generateInterfaceTemplate(data),
209
+ type: "data",
210
+ });
211
+
212
+ files.push({
213
+ path: paths.model,
214
+ content: generateModelTemplate(data),
215
+ type: "data",
216
+ });
217
+
218
+ files.push({
219
+ path: paths.service,
220
+ content: generateServiceTemplate(data),
221
+ type: "data",
222
+ });
223
+
224
+ files.push({
225
+ path: paths.fields,
226
+ content: generateFieldsTemplate(data),
227
+ type: "data",
228
+ });
229
+
230
+ // Form components (4 files)
231
+ files.push({
232
+ path: paths.editor,
233
+ content: generateEditorTemplate(data),
234
+ type: "component",
235
+ });
236
+
237
+ files.push({
238
+ path: paths.deleter,
239
+ content: generateDeleterTemplate(data),
240
+ type: "component",
241
+ });
242
+
243
+ files.push({
244
+ path: paths.selector,
245
+ content: generateSelectorTemplate(data),
246
+ type: "component",
247
+ });
248
+
249
+ files.push({
250
+ path: paths.multiSelector,
251
+ content: generateMultiSelectorTemplate(data),
252
+ type: "component",
253
+ });
254
+
255
+ // Display components (4-5 files)
256
+ files.push({
257
+ path: paths.list,
258
+ content: generateListTemplate(data),
259
+ type: "component",
260
+ });
261
+
262
+ files.push({
263
+ path: paths.details,
264
+ content: generateDetailsTemplate(data),
265
+ type: "component",
266
+ });
267
+
268
+ // Content component only for Content-extending modules
269
+ const contentTemplate = generateContentTemplate(data);
270
+ if (contentTemplate) {
271
+ files.push({
272
+ path: paths.content,
273
+ content: contentTemplate,
274
+ type: "component",
275
+ });
276
+ }
277
+
278
+ files.push({
279
+ path: paths.container,
280
+ content: generateContainerTemplate(data),
281
+ type: "component",
282
+ });
283
+
284
+ files.push({
285
+ path: paths.listContainer,
286
+ content: generateListContainerTemplate(data),
287
+ type: "component",
288
+ });
289
+
290
+ // Context & hooks (2 files)
291
+ files.push({
292
+ path: paths.context,
293
+ content: generateContextTemplate(data),
294
+ type: "context",
295
+ });
296
+
297
+ files.push({
298
+ path: paths.tableHook,
299
+ content: generateTableHookTemplate(data),
300
+ type: "hook",
301
+ });
302
+
303
+ // Module (1 file)
304
+ files.push({
305
+ path: paths.module,
306
+ content: generateModuleTemplate(data),
307
+ type: "module",
308
+ });
309
+
310
+ // Pages (2 files)
311
+ files.push({
312
+ path: paths.listPage,
313
+ content: generateListPageTemplate(data),
314
+ type: "page",
315
+ });
316
+
317
+ files.push({
318
+ path: paths.detailPage,
319
+ content: generateDetailPageTemplate(data),
320
+ type: "page",
321
+ });
322
+
323
+ return files;
324
+ }
325
+
326
+ /**
327
+ * Find the web app base path from the JSON path
328
+ * Assumes JSON is in /structure folder and web app is in /apps/web
329
+ */
330
+ function findWebBasePath(jsonPath: string): string {
331
+ // Try to find apps/web relative to current working directory
332
+ const cwd = process.cwd();
333
+
334
+ // Check if we're in the monorepo root
335
+ if (fs.existsSync(path.join(cwd, "apps/web"))) {
336
+ return cwd;
337
+ }
338
+
339
+ // Check if we're in packages/nextjs-jsonapi
340
+ if (fs.existsSync(path.join(cwd, "../../apps/web"))) {
341
+ return path.join(cwd, "../..");
342
+ }
343
+
344
+ // Try to derive from jsonPath
345
+ if (jsonPath) {
346
+ const resolved = path.resolve(jsonPath);
347
+ // Look for monorepo root by finding apps/web
348
+ let current = path.dirname(resolved);
349
+ for (let i = 0; i < 10; i++) {
350
+ if (fs.existsSync(path.join(current, "apps/web"))) {
351
+ return current;
352
+ }
353
+ const parent = path.dirname(current);
354
+ if (parent === current) break;
355
+ current = parent;
356
+ }
357
+ }
358
+
359
+ // Default to current directory
360
+ return cwd;
361
+ }
362
+
363
+ export default generateWebModule;
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Frontend Module Generator CLI
4
+ *
5
+ * Generates Next.js frontend modules from JSON schema definitions.
6
+ *
7
+ * Usage:
8
+ * pnpm generate-web-module <json-path> [options]
9
+ *
10
+ * Options:
11
+ * --dry-run Preview without writing files
12
+ * --force Overwrite existing files
13
+ * --no-register Skip Bootstrapper and i18n updates
14
+ */
15
+
16
+ import { Command } from "commander";
17
+ import * as path from "path";
18
+ import { generateWebModule } from "./generator";
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name("generate-web-module")
24
+ .description("Generate Next.js frontend module from JSON schema")
25
+ .version("1.0.0")
26
+ .argument("<json-path>", "Path to JSON schema file (relative or absolute)")
27
+ .option("-d, --dry-run", "Preview files without writing them", false)
28
+ .option("-f, --force", "Overwrite existing files", false)
29
+ .option("-n, --no-register", "Skip Bootstrapper.ts and i18n updates", false)
30
+ .action(async (jsonPath: string, options: { dryRun: boolean; force: boolean; register: boolean }) => {
31
+ // Resolve path
32
+ const resolvedPath = path.isAbsolute(jsonPath) ? jsonPath : path.resolve(process.cwd(), jsonPath);
33
+
34
+ try {
35
+ const success = await generateWebModule({
36
+ jsonPath: resolvedPath,
37
+ dryRun: options.dryRun,
38
+ force: options.force,
39
+ noRegister: !options.register,
40
+ });
41
+
42
+ process.exit(success ? 0 : 1);
43
+ } catch (error) {
44
+ console.error("\n❌ Error:", error instanceof Error ? error.message : error);
45
+ process.exit(1);
46
+ }
47
+ });
48
+
49
+ program.parse();
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Container Template
3
+ *
4
+ * Generates {Module}Container.tsx component for displaying item detail page.
5
+ */
6
+
7
+ import { FrontendTemplateData } from "../../types/template-data.interface";
8
+
9
+ /**
10
+ * Generate the container component file content
11
+ *
12
+ * @param data - Frontend template data
13
+ * @returns Generated file content
14
+ */
15
+ export function generateContainerTemplate(data: FrontendTemplateData): string {
16
+ const { names, extendsContent } = data;
17
+
18
+ // Content-extending modules get full tabs with relevant content/users
19
+ if (extendsContent) {
20
+ return generateContentContainerTemplate(data);
21
+ }
22
+
23
+ // Non-content modules get simpler container
24
+ return generateSimpleContainerTemplate(data);
25
+ }
26
+
27
+ /**
28
+ * Generate container for Content-extending modules
29
+ */
30
+ function generateContentContainerTemplate(data: FrontendTemplateData): string {
31
+ const { names } = data;
32
+
33
+ return `"use client";
34
+
35
+ import ${names.pascalCase}Content from "@/features/${data.targetDir}/${names.kebabCase}/components/details/${names.pascalCase}Content";
36
+ import ${names.pascalCase}Details from "@/features/${data.targetDir}/${names.kebabCase}/components/details/${names.pascalCase}Details";
37
+ import { use${names.pascalCase}Context } from "@/features/${data.targetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
38
+ import { ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
39
+ import {
40
+ AllowedUsersDetails,
41
+ PageContentContainer,
42
+ RelevantContentsList,
43
+ RelevantUsersList,
44
+ } from "@carlonicora/nextjs-jsonapi/components";
45
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@carlonicora/nextjs-jsonapi/shadcnui";
46
+ import { useTranslations } from "next-intl";
47
+
48
+ type ${names.pascalCase}ContainerProps = {
49
+ ${names.camelCase}: ${names.pascalCase}Interface;
50
+ };
51
+
52
+ function ${names.pascalCase}ContainerInternal({ ${names.camelCase} }: ${names.pascalCase}ContainerProps) {
53
+ const t = useTranslations();
54
+
55
+ return (
56
+ <PageContentContainer
57
+ details={<${names.pascalCase}Details />}
58
+ footer={
59
+ <AllowedUsersDetails showTitle content={${names.camelCase}} />
60
+ }
61
+ content={
62
+ <Tabs defaultValue={\`${names.camelCase}\`}>
63
+ <TabsList className="mb-4">
64
+ <TabsTrigger value="${names.camelCase}">{t(\`types.${names.pluralCamel}\`, { count: 1 })}</TabsTrigger>
65
+ <TabsTrigger value="contents">{t(\`generic.relevant\`)}</TabsTrigger>
66
+ <TabsTrigger value="users">{t(\`generic.relevant_users\`)}</TabsTrigger>
67
+ </TabsList>
68
+ <TabsContent value="${names.camelCase}">
69
+ <${names.pascalCase}Content />
70
+ </TabsContent>
71
+ <TabsContent value="contents">
72
+ <RelevantContentsList id={${names.camelCase}.id} />
73
+ </TabsContent>
74
+ <TabsContent value="users">
75
+ <RelevantUsersList id={${names.camelCase}.id} />
76
+ </TabsContent>
77
+ </Tabs>
78
+ }
79
+ />
80
+ );
81
+ }
82
+
83
+ export default function ${names.pascalCase}Container() {
84
+ const { ${names.camelCase} } = use${names.pascalCase}Context();
85
+ if (!${names.camelCase}) return null;
86
+
87
+ return <${names.pascalCase}ContainerInternal ${names.camelCase}={${names.camelCase}} />;
88
+ }
89
+ `;
90
+ }
91
+
92
+ /**
93
+ * Generate container for non-Content modules
94
+ */
95
+ function generateSimpleContainerTemplate(data: FrontendTemplateData): string {
96
+ const { names } = data;
97
+
98
+ return `"use client";
99
+
100
+ import ${names.pascalCase}Details from "@/features/${data.targetDir}/${names.kebabCase}/components/details/${names.pascalCase}Details";
101
+ import { use${names.pascalCase}Context } from "@/features/${data.targetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
102
+ import { ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
103
+ import { PageContentContainer } from "@carlonicora/nextjs-jsonapi/components";
104
+
105
+ type ${names.pascalCase}ContainerProps = {
106
+ ${names.camelCase}: ${names.pascalCase}Interface;
107
+ };
108
+
109
+ function ${names.pascalCase}ContainerInternal({ ${names.camelCase} }: ${names.pascalCase}ContainerProps) {
110
+ return (
111
+ <PageContentContainer
112
+ details={<${names.pascalCase}Details />}
113
+ content={
114
+ <div className="flex w-full flex-col gap-y-4">
115
+ {/* Add custom content sections here */}
116
+ </div>
117
+ }
118
+ />
119
+ );
120
+ }
121
+
122
+ export default function ${names.pascalCase}Container() {
123
+ const { ${names.camelCase} } = use${names.pascalCase}Context();
124
+ if (!${names.camelCase}) return null;
125
+
126
+ return <${names.pascalCase}ContainerInternal ${names.camelCase}={${names.camelCase}} />;
127
+ }
128
+ `;
129
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Content Template
3
+ *
4
+ * Generates {Module}Content.tsx component for displaying BlockNote content.
5
+ * Only generated for Content-extending modules.
6
+ */
7
+
8
+ import { FrontendTemplateData } from "../../types/template-data.interface";
9
+
10
+ /**
11
+ * Generate the content display component file content
12
+ *
13
+ * @param data - Frontend template data
14
+ * @returns Generated file content or null if not Content-extending
15
+ */
16
+ export function generateContentTemplate(data: FrontendTemplateData): string | null {
17
+ const { names, extendsContent } = data;
18
+
19
+ // Only generate for Content-extending modules
20
+ if (!extendsContent) {
21
+ return null;
22
+ }
23
+
24
+ return `"use client";
25
+
26
+ import { use${names.pascalCase}Context } from "@/features/${data.targetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
27
+ import { BlockNoteEditorContainer } from "@carlonicora/nextjs-jsonapi/components";
28
+ import { Card } from "@carlonicora/nextjs-jsonapi/shadcnui";
29
+
30
+ export default function ${names.pascalCase}Content() {
31
+ const { ${names.camelCase} } = use${names.pascalCase}Context();
32
+
33
+ return (
34
+ <Card className="flex w-full flex-col p-4">
35
+ <BlockNoteEditorContainer id={${names.camelCase}.id} type="${names.camelCase}" initialContent={${names.camelCase}.content} />
36
+ </Card>
37
+ );
38
+ }
39
+ `;
40
+ }