breeze-client 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +83 -63
  3. package/adapters/abstract-data-service-adapter.d.ts +120 -0
  4. package/adapters/abstract-data-service-adapter.js +395 -0
  5. package/adapters/abstract-data-service-adapter.js.map +1 -0
  6. package/adapters/adapter-ajax-fetch.d.ts +12 -0
  7. package/adapters/adapter-ajax-fetch.js +13 -0
  8. package/adapters/adapter-ajax-fetch.js.map +1 -0
  9. package/{adapter-ajax-post → adapters}/adapter-ajax-post.d.ts +40 -36
  10. package/{fesm2022/breeze-client-adapter-ajax-post.mjs → adapters/adapter-ajax-post.js} +80 -82
  11. package/adapters/adapter-ajax-post.js.map +1 -0
  12. package/{adapter-ajax-fetch → adapters}/adapter-core.d.ts +4 -4
  13. package/{fesm2022/breeze-client-adapter-uri-builder-json.mjs → adapters/adapter-core.js} +48 -91
  14. package/adapters/adapter-core.js.map +1 -0
  15. package/adapters/adapter-data-service-webapi.d.ts +9 -0
  16. package/{fesm2022/breeze-client-adapter-data-service-webapi.mjs → adapters/adapter-data-service-webapi.js} +105 -107
  17. package/adapters/adapter-data-service-webapi.js.map +1 -0
  18. package/adapters/adapter-model-library-backing-store.d.ts +32 -0
  19. package/adapters/adapter-model-library-backing-store.js +321 -0
  20. package/adapters/adapter-model-library-backing-store.js.map +1 -0
  21. package/adapters/adapter-uri-builder-json.d.ts +11 -0
  22. package/adapters/adapter-uri-builder-json.js +40 -0
  23. package/adapters/adapter-uri-builder-json.js.map +1 -0
  24. package/adapters/http.d.ts +52 -0
  25. package/adapters/http.js +202 -0
  26. package/adapters/http.js.map +1 -0
  27. package/angular/adapter-angular-httpclient.d.ts +21 -0
  28. package/angular/adapter-angular-httpclient.js +70 -0
  29. package/angular/adapter-angular-httpclient.js.map +1 -0
  30. package/breeze.d.ts +215 -0
  31. package/breeze.js +141 -0
  32. package/breeze.js.map +1 -0
  33. package/config/config.d.ts +212 -0
  34. package/config/config.js +392 -0
  35. package/config/config.js.map +1 -0
  36. package/config/configure.d.ts +66 -0
  37. package/config/configure.js +46 -0
  38. package/config/configure.js.map +1 -0
  39. package/config/default-adapters.d.ts +1 -0
  40. package/config/default-adapters.js +40 -0
  41. package/config/default-adapters.js.map +1 -0
  42. package/config/interface-registry.d.ts +191 -0
  43. package/config/interface-registry.js +5 -0
  44. package/config/interface-registry.js.map +1 -0
  45. package/{src → core}/assert-param.d.ts +52 -60
  46. package/core/assert-param.js +390 -0
  47. package/core/assert-param.js.map +1 -0
  48. package/core/core.d.ts +285 -0
  49. package/core/core.js +772 -0
  50. package/core/core.js.map +1 -0
  51. package/core/enum.d.ts +116 -0
  52. package/core/enum.js +167 -0
  53. package/core/enum.js.map +1 -0
  54. package/core/event.d.ts +169 -0
  55. package/core/event.js +338 -0
  56. package/core/event.js.map +1 -0
  57. package/entity/array.d.ts +3 -0
  58. package/entity/array.js +4 -0
  59. package/entity/array.js.map +1 -0
  60. package/entity/complex-array.d.ts +17 -0
  61. package/entity/complex-array.js +104 -0
  62. package/entity/complex-array.js.map +1 -0
  63. package/entity/default-property-interceptor.d.ts +1 -0
  64. package/entity/default-property-interceptor.js +496 -0
  65. package/entity/default-property-interceptor.js.map +1 -0
  66. package/{src → entity}/entity-action.d.ts +36 -42
  67. package/entity/entity-action.js +45 -0
  68. package/entity/entity-action.js.map +1 -0
  69. package/entity/entity-aspect.d.ts +457 -0
  70. package/entity/entity-aspect.js +1262 -0
  71. package/entity/entity-aspect.js.map +1 -0
  72. package/entity/entity-base.d.ts +55 -0
  73. package/entity/entity-base.js +41 -0
  74. package/entity/entity-base.js.map +1 -0
  75. package/entity/entity-group.d.ts +1 -0
  76. package/entity/entity-group.js +224 -0
  77. package/entity/entity-group.js.map +1 -0
  78. package/entity/entity-key.d.ts +95 -0
  79. package/entity/entity-key.js +189 -0
  80. package/entity/entity-key.js.map +1 -0
  81. package/{src → entity}/entity-state.d.ts +112 -84
  82. package/entity/entity-state.js +135 -0
  83. package/entity/entity-state.js.map +1 -0
  84. package/entity/key-generator.d.ts +57 -0
  85. package/entity/key-generator.js +155 -0
  86. package/entity/key-generator.js.map +1 -0
  87. package/entity/observable-array.d.ts +25 -0
  88. package/entity/observable-array.js +230 -0
  89. package/entity/observable-array.js.map +1 -0
  90. package/entity/primitive-array.d.ts +12 -0
  91. package/entity/primitive-array.js +71 -0
  92. package/entity/primitive-array.js.map +1 -0
  93. package/entity/relation-array.d.ts +24 -0
  94. package/entity/relation-array.js +176 -0
  95. package/entity/relation-array.js.map +1 -0
  96. package/entity/unattached-children-map.d.ts +1 -0
  97. package/entity/unattached-children-map.js +105 -0
  98. package/entity/unattached-children-map.js.map +1 -0
  99. package/generate-entity-classes.js +1176 -0
  100. package/manager/entity-manager.d.ts +801 -0
  101. package/manager/entity-manager.js +2395 -0
  102. package/manager/entity-manager.js.map +1 -0
  103. package/{src → manager}/save-options.d.ts +48 -42
  104. package/manager/save-options.js +51 -0
  105. package/manager/save-options.js.map +1 -0
  106. package/{src → metadata}/data-service.d.ts +229 -184
  107. package/metadata/data-service.js +246 -0
  108. package/metadata/data-service.js.map +1 -0
  109. package/metadata/data-type.d.ts +122 -0
  110. package/metadata/data-type.js +465 -0
  111. package/metadata/data-type.js.map +1 -0
  112. package/metadata/entity-metadata.d.ts +1193 -0
  113. package/metadata/entity-metadata.js +2502 -0
  114. package/metadata/entity-metadata.js.map +1 -0
  115. package/{src → metadata}/local-query-comparison-options.d.ts +65 -59
  116. package/metadata/local-query-comparison-options.js +69 -0
  117. package/metadata/local-query-comparison-options.js.map +1 -0
  118. package/{src → metadata}/naming-convention.d.ts +75 -72
  119. package/metadata/naming-convention.js +97 -0
  120. package/metadata/naming-convention.js.map +1 -0
  121. package/mixins/mixin-get-entity-graph.d.ts +63 -0
  122. package/{fesm2022/breeze-client-mixin-get-entity-graph.mjs → mixins/mixin-get-entity-graph.js} +331 -253
  123. package/mixins/mixin-get-entity-graph.js.map +1 -0
  124. package/mixins/mixin-save-queuing.d.ts +98 -0
  125. package/{fesm2022/breeze-client-mixin-save-queuing.mjs → mixins/mixin-save-queuing.js} +493 -365
  126. package/mixins/mixin-save-queuing.js.map +1 -0
  127. package/package.json +62 -93
  128. package/query/entity-query.d.ts +521 -0
  129. package/query/entity-query.js +1149 -0
  130. package/query/entity-query.js.map +1 -0
  131. package/{src → query}/mapping-context.d.ts +44 -46
  132. package/query/mapping-context.js +412 -0
  133. package/query/mapping-context.js.map +1 -0
  134. package/query/predicate.d.ts +394 -0
  135. package/query/predicate.js +1294 -0
  136. package/query/predicate.js.map +1 -0
  137. package/query/property-path.d.ts +252 -0
  138. package/query/property-path.js +2 -0
  139. package/query/property-path.js.map +1 -0
  140. package/{src → query}/query-options.d.ts +143 -119
  141. package/query/query-options.js +180 -0
  142. package/query/query-options.js.map +1 -0
  143. package/rxjs/breeze-rxjs.d.ts +72 -0
  144. package/rxjs/breeze-rxjs.js +117 -0
  145. package/rxjs/breeze-rxjs.js.map +1 -0
  146. package/validation/validate.d.ts +583 -0
  147. package/validation/validate.js +961 -0
  148. package/validation/validate.js.map +1 -0
  149. package/{src → validation}/validation-options.d.ts +67 -63
  150. package/validation/validation-options.js +70 -0
  151. package/validation/validation-options.js.map +1 -0
  152. package/adapter-ajax-angularjs/adapter-ajax-angularjs.d.ts +0 -17
  153. package/adapter-ajax-angularjs/adapter-core.d.ts +0 -4
  154. package/adapter-ajax-angularjs/index.d.ts +0 -5
  155. package/adapter-ajax-fetch/adapter-ajax-fetch.d.ts +0 -18
  156. package/adapter-ajax-fetch/index.d.ts +0 -5
  157. package/adapter-ajax-httpclient/adapter-ajax-httpclient.d.ts +0 -13
  158. package/adapter-ajax-httpclient/adapter-core.d.ts +0 -4
  159. package/adapter-ajax-httpclient/index.d.ts +0 -5
  160. package/adapter-ajax-jquery/adapter-ajax-jquery.d.ts +0 -15
  161. package/adapter-ajax-jquery/index.d.ts +0 -5
  162. package/adapter-ajax-post/index.d.ts +0 -5
  163. package/adapter-data-service-odata/adapter-core.d.ts +0 -4
  164. package/adapter-data-service-odata/adapter-data-service-odata.d.ts +0 -17
  165. package/adapter-data-service-odata/index.d.ts +0 -5
  166. package/adapter-data-service-webapi/adapter-data-service-webapi.d.ts +0 -15
  167. package/adapter-data-service-webapi/index.d.ts +0 -5
  168. package/adapter-model-library-backing-store/adapter-model-library-backing-store.d.ts +0 -10
  169. package/adapter-model-library-backing-store/index.d.ts +0 -5
  170. package/adapter-model-library-ko/adapter-model-library-ko.d.ts +0 -10
  171. package/adapter-model-library-ko/index.d.ts +0 -5
  172. package/adapter-uri-builder-json/adapter-core.d.ts +0 -4
  173. package/adapter-uri-builder-json/adapter-uri-builder-json.d.ts +0 -8
  174. package/adapter-uri-builder-json/index.d.ts +0 -5
  175. package/adapter-uri-builder-odata/adapter-core.d.ts +0 -4
  176. package/adapter-uri-builder-odata/adapter-uri-builder-odata.d.ts +0 -8
  177. package/adapter-uri-builder-odata/index.d.ts +0 -5
  178. package/esm2022/adapter-ajax-angularjs/adapter-ajax-angularjs.mjs +0 -135
  179. package/esm2022/adapter-ajax-angularjs/adapter-core.mjs +0 -49
  180. package/esm2022/adapter-ajax-angularjs/breeze-client-adapter-ajax-angularjs.mjs +0 -5
  181. package/esm2022/adapter-ajax-fetch/adapter-ajax-fetch.mjs +0 -135
  182. package/esm2022/adapter-ajax-fetch/adapter-core.mjs +0 -49
  183. package/esm2022/adapter-ajax-fetch/breeze-client-adapter-ajax-fetch.mjs +0 -5
  184. package/esm2022/adapter-ajax-httpclient/adapter-ajax-httpclient.mjs +0 -139
  185. package/esm2022/adapter-ajax-httpclient/adapter-core.mjs +0 -49
  186. package/esm2022/adapter-ajax-httpclient/breeze-client-adapter-ajax-httpclient.mjs +0 -5
  187. package/esm2022/adapter-ajax-jquery/adapter-ajax-jquery.mjs +0 -107
  188. package/esm2022/adapter-ajax-jquery/breeze-client-adapter-ajax-jquery.mjs +0 -5
  189. package/esm2022/adapter-ajax-post/adapter-ajax-post.mjs +0 -77
  190. package/esm2022/adapter-ajax-post/breeze-client-adapter-ajax-post.mjs +0 -5
  191. package/esm2022/adapter-data-service-odata/adapter-core.mjs +0 -49
  192. package/esm2022/adapter-data-service-odata/adapter-data-service-odata.mjs +0 -461
  193. package/esm2022/adapter-data-service-odata/breeze-client-adapter-data-service-odata.mjs +0 -5
  194. package/esm2022/adapter-data-service-webapi/adapter-data-service-webapi.mjs +0 -100
  195. package/esm2022/adapter-data-service-webapi/breeze-client-adapter-data-service-webapi.mjs +0 -5
  196. package/esm2022/adapter-model-library-backing-store/adapter-model-library-backing-store.mjs +0 -269
  197. package/esm2022/adapter-model-library-backing-store/breeze-client-adapter-model-library-backing-store.mjs +0 -5
  198. package/esm2022/adapter-model-library-ko/adapter-model-library-ko.mjs +0 -241
  199. package/esm2022/adapter-model-library-ko/breeze-client-adapter-model-library-ko.mjs +0 -5
  200. package/esm2022/adapter-uri-builder-json/adapter-core.mjs +0 -49
  201. package/esm2022/adapter-uri-builder-json/adapter-uri-builder-json.mjs +0 -37
  202. package/esm2022/adapter-uri-builder-json/breeze-client-adapter-uri-builder-json.mjs +0 -5
  203. package/esm2022/adapter-uri-builder-odata/adapter-core.mjs +0 -49
  204. package/esm2022/adapter-uri-builder-odata/adapter-uri-builder-odata.mjs +0 -191
  205. package/esm2022/adapter-uri-builder-odata/breeze-client-adapter-uri-builder-odata.mjs +0 -5
  206. package/esm2022/breeze-client.mjs +0 -5
  207. package/esm2022/index.mjs +0 -2
  208. package/esm2022/mixin-get-entity-graph/breeze-client-mixin-get-entity-graph.mjs +0 -5
  209. package/esm2022/mixin-get-entity-graph/mixin-get-entity-graph.mjs +0 -269
  210. package/esm2022/mixin-save-queuing/breeze-client-mixin-save-queuing.mjs +0 -5
  211. package/esm2022/mixin-save-queuing/mixin-save-queuing.mjs +0 -421
  212. package/esm2022/src/abstract-data-service-adapter.mjs +0 -325
  213. package/esm2022/src/array.mjs +0 -4
  214. package/esm2022/src/assert-param.mjs +0 -330
  215. package/esm2022/src/breeze.mjs +0 -85
  216. package/esm2022/src/complex-array.mjs +0 -117
  217. package/esm2022/src/config.mjs +0 -232
  218. package/esm2022/src/core.mjs +0 -712
  219. package/esm2022/src/csdl-metadata-parser.mjs +0 -385
  220. package/esm2022/src/data-service.mjs +0 -221
  221. package/esm2022/src/data-type.mjs +0 -510
  222. package/esm2022/src/default-property-interceptor.mjs +0 -460
  223. package/esm2022/src/entity-action.mjs +0 -45
  224. package/esm2022/src/entity-aspect.mjs +0 -897
  225. package/esm2022/src/entity-group.mjs +0 -192
  226. package/esm2022/src/entity-key.mjs +0 -125
  227. package/esm2022/src/entity-manager.mjs +0 -2163
  228. package/esm2022/src/entity-metadata.mjs +0 -2242
  229. package/esm2022/src/entity-query.mjs +0 -1024
  230. package/esm2022/src/entity-state.mjs +0 -107
  231. package/esm2022/src/enum.mjs +0 -143
  232. package/esm2022/src/event.mjs +0 -264
  233. package/esm2022/src/interface-registry.mjs +0 -39
  234. package/esm2022/src/key-generator.mjs +0 -110
  235. package/esm2022/src/local-query-comparison-options.mjs +0 -65
  236. package/esm2022/src/mapping-context.mjs +0 -411
  237. package/esm2022/src/naming-convention.mjs +0 -92
  238. package/esm2022/src/observable-array.mjs +0 -183
  239. package/esm2022/src/predicate.mjs +0 -1259
  240. package/esm2022/src/primitive-array.mjs +0 -85
  241. package/esm2022/src/query-options.mjs +0 -152
  242. package/esm2022/src/relation-array.mjs +0 -173
  243. package/esm2022/src/save-options.mjs +0 -41
  244. package/esm2022/src/unattached-children-map.mjs +0 -56
  245. package/esm2022/src/validate.mjs +0 -995
  246. package/esm2022/src/validation-options.mjs +0 -64
  247. package/fesm2022/breeze-client-adapter-ajax-angularjs.mjs +0 -190
  248. package/fesm2022/breeze-client-adapter-ajax-angularjs.mjs.map +0 -1
  249. package/fesm2022/breeze-client-adapter-ajax-fetch.mjs +0 -190
  250. package/fesm2022/breeze-client-adapter-ajax-fetch.mjs.map +0 -1
  251. package/fesm2022/breeze-client-adapter-ajax-httpclient.mjs +0 -194
  252. package/fesm2022/breeze-client-adapter-ajax-httpclient.mjs.map +0 -1
  253. package/fesm2022/breeze-client-adapter-ajax-jquery.mjs +0 -114
  254. package/fesm2022/breeze-client-adapter-ajax-jquery.mjs.map +0 -1
  255. package/fesm2022/breeze-client-adapter-ajax-post.mjs.map +0 -1
  256. package/fesm2022/breeze-client-adapter-data-service-odata.mjs +0 -516
  257. package/fesm2022/breeze-client-adapter-data-service-odata.mjs.map +0 -1
  258. package/fesm2022/breeze-client-adapter-data-service-webapi.mjs.map +0 -1
  259. package/fesm2022/breeze-client-adapter-model-library-backing-store.mjs +0 -276
  260. package/fesm2022/breeze-client-adapter-model-library-backing-store.mjs.map +0 -1
  261. package/fesm2022/breeze-client-adapter-model-library-ko.mjs +0 -248
  262. package/fesm2022/breeze-client-adapter-model-library-ko.mjs.map +0 -1
  263. package/fesm2022/breeze-client-adapter-uri-builder-json.mjs.map +0 -1
  264. package/fesm2022/breeze-client-adapter-uri-builder-odata.mjs +0 -246
  265. package/fesm2022/breeze-client-adapter-uri-builder-odata.mjs.map +0 -1
  266. package/fesm2022/breeze-client-mixin-get-entity-graph.mjs.map +0 -1
  267. package/fesm2022/breeze-client-mixin-save-queuing.mjs.map +0 -1
  268. package/fesm2022/breeze-client.mjs +0 -14144
  269. package/fesm2022/breeze-client.mjs.map +0 -1
  270. package/index.d.ts +0 -1
  271. package/mixin-get-entity-graph/index.d.ts +0 -5
  272. package/mixin-get-entity-graph/mixin-get-entity-graph.d.ts +0 -31
  273. package/mixin-save-queuing/index.d.ts +0 -5
  274. package/mixin-save-queuing/mixin-save-queuing.d.ts +0 -32
  275. package/src/abstract-data-service-adapter.d.ts +0 -84
  276. package/src/array.d.ts +0 -3
  277. package/src/breeze.d.ts +0 -139
  278. package/src/complex-array.d.ts +0 -13
  279. package/src/config.d.ts +0 -104
  280. package/src/core.d.ts +0 -164
  281. package/src/csdl-metadata-parser.d.ts +0 -7
  282. package/src/data-type.d.ts +0 -89
  283. package/src/default-property-interceptor.d.ts +0 -4
  284. package/src/entity-aspect.d.ts +0 -332
  285. package/src/entity-group.d.ts +0 -28
  286. package/src/entity-key.d.ts +0 -77
  287. package/src/entity-manager.d.ts +0 -680
  288. package/src/entity-metadata.d.ts +0 -988
  289. package/src/entity-query.d.ts +0 -444
  290. package/src/enum.d.ts +0 -100
  291. package/src/event.d.ts +0 -142
  292. package/src/interface-registry.d.ts +0 -150
  293. package/src/key-generator.d.ts +0 -12
  294. package/src/observable-array.d.ts +0 -52
  295. package/src/predicate.d.ts +0 -376
  296. package/src/primitive-array.d.ts +0 -7
  297. package/src/relation-array.d.ts +0 -18
  298. package/src/unattached-children-map.d.ts +0 -19
  299. package/src/validate.d.ts +0 -650
@@ -0,0 +1,1176 @@
1
+ #!/usr/bin/env node
2
+ // Generate - or update in place - one TypeScript class per structural type in a Breeze
3
+ // metadata document.
4
+ //
5
+ // npx breeze-gen-entities --out src/app/model \
6
+ // --service http://localhost:34377/breeze/NorthwindIBModel
7
+ //
8
+ // That is the spelling for an application: the script is published inside breeze-client,
9
+ // wired up as the `breeze-gen-entities` bin, so `npm i breeze-client` is the whole install.
10
+ // Inside this repo it is also `node scripts/generate-entity-classes.js ...`, and
11
+ // `npm run gen:model` is that with the arguments filled in.
12
+ //
13
+ // --out and a metadata source are required; nothing can infer either. Everything else has a
14
+ // default - the files import from 'breeze-client', which is right wherever the package is
15
+ // installed. Paths are relative to where the command is run, not to where the script lives.
16
+ //
17
+ // It reads the metadata through the library itself, so naming conventions, `nameOnServer`,
18
+ // inheritance and complex types resolve exactly as they do at runtime. See loadBreeze for
19
+ // which copy of Breeze that is.
20
+ //
21
+ // Updating is per member, not per file, and THE METADATA DECIDES WHAT THE GENERATOR OWNS - not
22
+ // the `// @generated` marker:
23
+ //
24
+ // - A declared property whose name is in the metadata is a mapped property of the type. It is
25
+ // rewritten to `declare <name>: <type>; // @generated`, whether or not it was marked.
26
+ // - A metadata property the file does not declare is appended.
27
+ // - A declared property the metadata does NOT have is the caller's and stays - unless it is
28
+ // marked, which means the generator wrote it and the column has left the schema, so it goes.
29
+ // That one decision is the only thing the marker still drives.
30
+ // - The class declaration is made to extend the base the generator means it to, and members
31
+ // that base supplies are not left redeclared.
32
+ // - Imports are added when a generated member needs one, and removed only when marked and
33
+ // unreferenced.
34
+ // - Methods, getters, constructors, unmapped properties, comments and hand-written imports
35
+ // survive untouched.
36
+ //
37
+ // Ownership by metadata is what lets a hand-written class be taken over a property at a time
38
+ // rather than having its whole body appended a second time. Because that rewrites lines somebody
39
+ // typed, a file with no `@generated-by` header is reported and skipped unless --adopt is passed.
40
+ //
41
+ // To keep something out of all of this, see MANUAL_MARK below: `// @manual` on one declaration,
42
+ // `// @manual-start` / `// @manual-end` around a block, `// @manual-file` for a whole file.
43
+ //
44
+ // Every generated file carries the generator version in its header, so a later version can tell
45
+ // what produced what. See test/model/README.md.
46
+ //
47
+ // --- why this is JavaScript, in a TypeScript repository ---------------------------------------
48
+ //
49
+ // So that it runs unbuilt, from wherever it happens to be. Two copies exist and both have to
50
+ // work the moment they are invoked: scripts/generate-entity-classes.js in this repo, and
51
+ // node_modules/breeze-client/generate-entity-classes.js after an install, which prepare-dist.mjs
52
+ // copies into dist/ and package.json declares as the `breeze-gen-entities` bin. Node runs this
53
+ // file directly in both places. tsconfig.json is `include: ["src/**/*.ts"]`, so scripts/ is
54
+ // outside the build graph entirely, and nothing about the tool can be stale against its source.
55
+ //
56
+ // It could be TypeScript - tsc already runs, and emitting it into dist/ next to breeze.js would
57
+ // work. The cost is a coupling that points the wrong way: the generator would not run until the
58
+ // library had been compiled, and its whole job is to run *before* an application has any model
59
+ // code to compile. It would also put a build step between an edit to this file and testing it,
60
+ // for a script whose only consumer is node.
61
+ //
62
+ // What that gives up is type checking, and the mitigation is test/unit/entity-generator.spec.ts,
63
+ // which runs the real script against real metadata in a temp directory and asserts on the files
64
+ // that come out - closer to what actually matters here than types on the string handling would be.
65
+ //
66
+ // The one place types would genuinely help is the Breeze metadata objects this reads through
67
+ // loadBreeze (EntityType, DataProperty, NavigationProperty). Those are `any` today. If that
68
+ // starts causing mistakes rather than just costing autocomplete, revisit it - a JSDoc
69
+ // `@type {import('../src/breeze').EntityType}` buys most of it with no build step, and
70
+ // `checkJs` would enforce it.
71
+
72
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
73
+ import { join, dirname, resolve } from 'node:path';
74
+ import { fileURLToPath } from 'node:url';
75
+
76
+ const GENERATOR_VERSION = '1.1.0';
77
+ const GENERATOR_NAME = 'generate-entity-classes';
78
+
79
+ /** Where the script itself lives: node_modules/breeze-client/ when installed, scripts/ here. */
80
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
81
+
82
+ /**
83
+ * What --out and --metadata are relative to. The caller's directory, never the script's: an
84
+ * installed generator sits in node_modules, and resolving --out against that would write the
85
+ * application's model into its own dependency. `npm run` sets cwd to the package root, so
86
+ * `npm run gen:model` is unaffected.
87
+ */
88
+ const workingDir = process.cwd();
89
+
90
+ /** The marker that says "this line is mine". */
91
+ const MARK = '// @generated';
92
+
93
+ /**
94
+ * The opt-out, at three scopes. Anything they cover is never rewritten, removed or re-pointed,
95
+ * whatever the metadata says, and nothing is inserted inside a region.
96
+ *
97
+ * // @manual on one declaration - that property is yours
98
+ * // @manual-start ... // @manual-end - everything between them is yours
99
+ * // @manual-file anywhere in the file - the whole file is yours
100
+ *
101
+ * They exist because the metadata - not the `@generated` marker - decides what the generator
102
+ * owns. A property whose name is in the metadata is the generator's, marked or not, which is what
103
+ * lets a hand-written class be adopted without editing every line of it first. That leaves no way
104
+ * to say "this one is mine" by deleting a marker, so it is said explicitly instead.
105
+ */
106
+ const MANUAL_MARK = '// @manual';
107
+ const MANUAL_FILE_MARK = '// @manual-file';
108
+ const MANUAL_START_MARK = '// @manual-start';
109
+ const MANUAL_END_MARK = '// @manual-end';
110
+
111
+ // --- options ---------------------------------------------------------------------------------
112
+
113
+ // Required: --out, and one of --metadata / --service. Neither is guessable, and guessing wrong
114
+ // at --out overwrites a directory the caller did not mean to name.
115
+ const DEFAULTS = {
116
+ metadata: null,
117
+ service: null,
118
+ out: null,
119
+ // The module the generated files import Breeze types from. The published package name is
120
+ // right for everyone who installs breeze-client, including this repo - test/tsconfig.json
121
+ // maps it to the sources with `paths`, and vitest.shared.config.ts with an alias. Override
122
+ // it only for a fork republished under another name.
123
+ breeze: 'breeze-client',
124
+ // Extension for sibling imports. '' suits a bundler (Vite, the test tier); '.js' suits a
125
+ // NodeNext project.
126
+ ext: '',
127
+ // A base class of the caller's own for every generated root type, and the module to import it
128
+ // from. Null means Breeze's own EntityBase / ComplexObjectBase, imported from --breeze.
129
+ base: null,
130
+ baseModule: null,
131
+ complexBase: null,
132
+ complexBaseModule: null,
133
+ nullable: false,
134
+ types: null,
135
+ index: true,
136
+ dryRun: false,
137
+ // Whether to take over a file the generator has never written - one with no `@generated-by`
138
+ // header. Claiming members in somebody's hand-written class is a one-way door with no undo but
139
+ // git, so a plain run reports what it would do and writes nothing.
140
+ adopt: false,
141
+ };
142
+
143
+ function parseArgs(argv) {
144
+ const opts = { ...DEFAULTS };
145
+ for (let i = 0; i < argv.length; i++) {
146
+ const arg = argv[i];
147
+ const next = () => {
148
+ const v = argv[++i];
149
+ if (v === undefined) fail(`${arg} needs a value`);
150
+ return v;
151
+ };
152
+ switch (arg) {
153
+ case '--metadata': opts.metadata = next(); opts.service = null; break;
154
+ case '--service': opts.service = next(); opts.metadata = null; break;
155
+ case '--out': opts.out = next(); break;
156
+ case '--breeze': opts.breeze = next(); break;
157
+ case '--ext': opts.ext = next(); break;
158
+ case '--base': opts.base = next(); break;
159
+ case '--base-module': opts.baseModule = next(); break;
160
+ case '--complex-base': opts.complexBase = next(); break;
161
+ case '--complex-base-module': opts.complexBaseModule = next(); break;
162
+ case '--types': opts.types = next().split(',').map(s => s.trim()).filter(Boolean); break;
163
+ case '--nullable': opts.nullable = true; break;
164
+ case '--no-index': opts.index = false; break;
165
+ case '--adopt': opts.adopt = true; break;
166
+ case '--dry-run': case '-n': opts.dryRun = true; break;
167
+ case '--version': console.log(GENERATOR_VERSION); process.exit(0);
168
+ case '--help': case '-h': usage(); process.exit(0);
169
+ default: fail(`unknown option ${arg}`);
170
+ }
171
+ }
172
+ if (!opts.metadata && !opts.service) fail('one of --metadata <file> or --service <url> is required');
173
+ if (!opts.out) fail('--out <dir> is required');
174
+ return opts;
175
+ }
176
+
177
+ function usage() {
178
+ console.log(`${GENERATOR_NAME} v${GENERATOR_VERSION} - TypeScript entity classes from Breeze metadata.
179
+
180
+ Required - one source of metadata:
181
+ --metadata <file> metadata JSON to read, relative to the current directory
182
+ --service <url> fetch <url>/Metadata from a running service instead
183
+
184
+ Required:
185
+ --out <dir> where the classes go, relative to the current directory
186
+
187
+ Optional:
188
+ --ext <ext> extension on sibling imports, e.g. .js for a NodeNext project
189
+ (default: none, which suits a bundler)
190
+
191
+ --breeze <spec> the module the generated files import Breeze types from
192
+ (default: breeze-client). Override it only for a fork republished
193
+ under another name. A relative specifier is written verbatim into
194
+ every generated file, so it must be correct relative to --out rather
195
+ than to where the command is run.
196
+
197
+ --base <Name> a base class of your own for every generated entity, so you can give them
198
+ all behaviour and still regenerate their properties. Only the root of an
199
+ inheritance chain extends it; a type with a metadata base type extends
200
+ that, as before.
201
+
202
+ It is scaffolded once, extending Breeze's EntityBase, and then never
203
+ rewritten - nothing in it is marked @generated:
204
+
205
+ // --base AppEntityBase -> app-entity-base.ts
206
+ export abstract class AppEntityBase extends EntityBase {
207
+ get isNew() { return this.entityAspect.entityState.isAdded(); }
208
+ }
209
+
210
+ --base-module <spec> where to import it from
211
+ (default: ./<kebab-name>, alongside the generated classes)
212
+ --complex-base <Name> the same, for complex types; extends ComplexObjectBase
213
+ --complex-base-module <spec>
214
+
215
+ --types <A,B> only these short names
216
+ --nullable add "| null" to nullable data properties
217
+ --no-index do not write index.ts
218
+
219
+ --adopt take over hand-written classes - files with no @generated-by header.
220
+ The metadata decides what is a mapped property, so those are rewritten
221
+ into the generated form and the class is made to extend the base;
222
+ methods, getters, constructors and everything else are left alone.
223
+ Without it such a file is reported and skipped. Pair with --dry-run
224
+ the first time.
225
+
226
+ To keep code away from the generator for good:
227
+ // @manual on one declaration
228
+ // @manual-start ... // @manual-end around a block
229
+ // @manual-file anywhere in a file - it is never opened
230
+ --dry-run, -n report what would change, write nothing
231
+ --version print the generator version
232
+ --help, -h this list
233
+
234
+ Examples:
235
+ npx breeze-gen-entities \\
236
+ --service http://localhost:34377/breeze/NorthwindIBModel \\
237
+ --out src/app/model
238
+
239
+ node scripts/generate-entity-classes.js \\
240
+ --metadata test/support/NorthwindIBMetadata_ETNOPAYLOAD.json \\
241
+ --out test/model`);
242
+ }
243
+
244
+ function fail(msg) {
245
+ console.error(`${GENERATOR_NAME}: ${msg}`);
246
+ process.exit(1);
247
+ }
248
+
249
+ // --- metadata --------------------------------------------------------------------------------
250
+
251
+ /**
252
+ * The copy of Breeze the metadata is read through - and it has to be the caller's own.
253
+ * Metadata parsing is Breeze's: naming conventions, `nameOnServer`, inheritance and complex
254
+ * types all resolve here exactly as they will at runtime, so a generator reading a different
255
+ * version would emit classes that disagree with the library the application runs.
256
+ *
257
+ * Installed, the script's sibling IS that copy: npx runs node_modules/breeze-client/
258
+ * generate-entity-classes.js, and breeze.js sits beside it in the same package. In this repo
259
+ * the script is in scripts/ and the build output is ../dist/.
260
+ */
261
+ async function loadBreeze() {
262
+ const candidates = [
263
+ join(scriptDir, 'breeze.js'), // installed: dist/ is the package root
264
+ join(scriptDir, '..', 'dist', 'breeze.js'), // this repo, after `npm run build`
265
+ ];
266
+ const found = candidates.find(existsSync);
267
+ if (!found) {
268
+ fail(`breeze.js not found - looked in:\n`
269
+ + candidates.map(c => ` ${c}`).join('\n')
270
+ + '\n In the breeze-client repo, run `npm run build` first.');
271
+ }
272
+ return import(`file://${found}`);
273
+ }
274
+
275
+ async function loadMetadata(opts) {
276
+ if (opts.service) {
277
+ const url = opts.service.replace(/\/$/, '') + '/Metadata';
278
+ const response = await fetch(url);
279
+ if (!response.ok) fail(`${url} returned ${response.status} ${response.statusText}`);
280
+ return await response.text();
281
+ }
282
+ const path = resolve(workingDir, opts.metadata);
283
+ if (!existsSync(path)) fail(`metadata file not found: ${path}`);
284
+ return readFileSync(path, 'utf8');
285
+ }
286
+
287
+ // --- names and types -------------------------------------------------------------------------
288
+
289
+ /** 'OrderDetail' -> 'order-detail', matching the file naming everywhere else in the repo. */
290
+ function kebab(name) {
291
+ return name
292
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
293
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
294
+ .toLowerCase();
295
+ }
296
+
297
+ /** 'Order:#Foo' -> 'Order' */
298
+ function shortNameOf(qualifiedName) {
299
+ return String(qualifiedName).split(':#')[0];
300
+ }
301
+
302
+ // What each Breeze DataType looks like once it reaches the client.
303
+ const TS_TYPE_BY_DATA_TYPE = {
304
+ String: 'string',
305
+ Guid: 'string',
306
+ Boolean: 'boolean',
307
+ Byte: 'number',
308
+ Int16: 'number',
309
+ Int32: 'number',
310
+ Int64: 'number',
311
+ Decimal: 'number',
312
+ Double: 'number',
313
+ Single: 'number',
314
+ DateTime: 'Date',
315
+ DateTimeOffset: 'Date',
316
+ DateOnly: 'Date',
317
+ // A .NET TimeSpan, as an ISO 8601 duration: "PT1H30M".
318
+ Time: 'string',
319
+ // A .NET TimeOnly, as the server writes it: "14:30:00".
320
+ TimeOnly: 'string',
321
+ // A byte[], base64 encoded.
322
+ Binary: 'string',
323
+ Undefined: 'any',
324
+ };
325
+
326
+ /** The TS type for one member, recording the imports it needs in `needs`. */
327
+ function memberType(prop, isNavigation, opts, needs) {
328
+ if (isNavigation) {
329
+ const target = shortNameOf(prop.entityTypeName);
330
+ needs.siblings.add(target);
331
+ if (prop.isScalar) return target;
332
+ needs.breeze.add('RelationArray');
333
+ return `RelationArray<${target}>`;
334
+ }
335
+ if (prop.isComplexProperty) {
336
+ const target = shortNameOf(prop.complexTypeName);
337
+ needs.siblings.add(target);
338
+ if (prop.isScalar === false) {
339
+ needs.breeze.add('ComplexArray');
340
+ return `ComplexArray<${target}>`;
341
+ }
342
+ return target;
343
+ }
344
+ const dataTypeName = prop.dataType && prop.dataType.name;
345
+ const tsType = TS_TYPE_BY_DATA_TYPE[dataTypeName];
346
+ if (!tsType) {
347
+ console.warn(` ! ${prop.parentType.shortName}.${prop.name}: unmapped DataType '${dataTypeName}', using any`);
348
+ return 'any';
349
+ }
350
+ if (!prop.isScalar) return `${tsType}[]`;
351
+ if (opts.nullable && prop.isNullable && tsType !== 'any') return `${tsType} | null`;
352
+ return tsType;
353
+ }
354
+
355
+ /**
356
+ * The members to write for one structural type: its own data properties, then its own navigation
357
+ * properties, in metadata order. Properties inherited from a base type are left to the base
358
+ * class, which the generated class extends.
359
+ */
360
+ function membersOf(stype, opts, needs) {
361
+ const members = [];
362
+ for (const dp of stype.dataProperties) {
363
+ if (dp.baseProperty) continue; // declared by the base class
364
+ if (dp.isUnmapped) continue; // comes from a client-side class, not from the server
365
+ members.push({ name: dp.name, type: memberType(dp, false, opts, needs) });
366
+ }
367
+ for (const np of stype.navigationProperties || []) {
368
+ if (np.baseProperty) continue;
369
+ members.push({ name: np.name, type: memberType(np, true, opts, needs) });
370
+ }
371
+ return members;
372
+ }
373
+
374
+ // --- reading an existing file ------------------------------------------------------------------
375
+
376
+ /**
377
+ * Blank out string, template and comment spans so that a brace scan cannot be fooled by one.
378
+ * Only used for locating things; the original text is what gets edited.
379
+ */
380
+ function maskLiterals(source) {
381
+ const out = source.split('');
382
+ let i = 0;
383
+ while (i < source.length) {
384
+ const c = source[i];
385
+ const two = source.substr(i, 2);
386
+ if (two === '//') {
387
+ while (i < source.length && source[i] !== '\n') { out[i] = ' '; i++; }
388
+ } else if (two === '/*') {
389
+ const end = source.indexOf('*/', i + 2);
390
+ const stop = end === -1 ? source.length : end + 2;
391
+ for (; i < stop; i++) if (source[i] !== '\n') out[i] = ' ';
392
+ } else if (c === '"' || c === "'" || c === '`') {
393
+ const quote = c;
394
+ out[i] = ' '; i++;
395
+ while (i < source.length) {
396
+ if (source[i] === '\\') { out[i] = ' '; out[i + 1] = ' '; i += 2; continue; }
397
+ if (source[i] === quote) { out[i] = ' '; i++; break; }
398
+ if (source[i] !== '\n') out[i] = ' ';
399
+ i++;
400
+ }
401
+ } else {
402
+ i++;
403
+ }
404
+ }
405
+ return out.join('');
406
+ }
407
+
408
+ /** The 0-based [first, last] line range of the body of `class <name>`, excluding the braces. */
409
+ function findClassBodyLines(lines, className) {
410
+ const masked = maskLiterals(lines.join('\n')).split('\n');
411
+ const declLine = masked.findIndex(l => new RegExp(`\\bclass\\s+${className}\\b`).test(l));
412
+ if (declLine === -1) return null;
413
+ let depth = 0;
414
+ let openLine = -1;
415
+ for (let i = declLine; i < masked.length; i++) {
416
+ for (const ch of masked[i]) {
417
+ if (ch === '{') { if (depth === 0) openLine = i; depth++; }
418
+ else if (ch === '}') {
419
+ depth--;
420
+ if (depth === 0) return { first: openLine + 1, last: i - 1 };
421
+ }
422
+ }
423
+ }
424
+ return null;
425
+ }
426
+
427
+ /**
428
+ * One member declaration, capturing indent, name and trailing comment:
429
+ * `declare customerID: string;` and the shapes a hand edit is likely to produce (modifiers, `?`,
430
+ * `!`, a trailing comment). Deliberately single-line, and deliberately blind to methods, getters
431
+ * and initialized fields - none of those is ever what this tool owns. A declaration wrapped
432
+ * across lines is not matched either; it reads as "not declared", so the generator appends a
433
+ * duplicate and tsc rejects it, rather than the edit going silently wrong.
434
+ */
435
+ const ANY_DECLARATION_RE =
436
+ /^([ \t]*)((?:(?:public|private|protected|readonly|declare|static|abstract)\s+)*)([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*([^;\n{]*);[ \t]*(\/\/.*)?$/;
437
+
438
+ /** [indent, modifiers, name, type] for a declaration line, or null. */
439
+ function parseDeclaration(line) {
440
+ const m = ANY_DECLARATION_RE.exec(line);
441
+ return m && { indent: m[1], modifiers: m[2], name: m[3], type: m[4] };
442
+ }
443
+
444
+ /**
445
+ * Whether a line already says what the generator would write, ignoring how it is spaced.
446
+ *
447
+ * Formatters reach these files. Prettier collapses the two spaces before the marker to one and
448
+ * flips quote style; it does not move or drop the trailing comment, even past 110 columns - both
449
+ * checked. Comparing the rendered strings byte for byte would therefore see a difference on every
450
+ * run and rewrite all of them back, so the generator and the formatter would each undo the other
451
+ * forever. Comparing meaning instead means a formatted file is simply left alone.
452
+ */
453
+ function isCanonical(line, member) {
454
+ const d = parseDeclaration(line);
455
+ if (!d || d.name !== member.name) return false;
456
+ if (!/\bdeclare\b/.test(d.modifiers)) return false; // missing `declare` is a real defect
457
+ if (!isMarked(line)) return false; // must still say whose it is
458
+ const squash = t => t.replace(/\s+/g, '');
459
+ return squash(d.type) === squash(member.type);
460
+ }
461
+
462
+ function isMarked(line) {
463
+ return /\/\/\s*@generated\b/.test(line);
464
+ }
465
+
466
+ /** `// @manual` on this line - and not `@manual-start`, `@manual-end` or `@manual-file`. */
467
+ function isManual(line) {
468
+ return /\/\/\s*@manual(?![-\w])/.test(line);
469
+ }
470
+
471
+ // The region and file markers must stand alone on their own comment line - anchored, so that
472
+ // prose mentioning one does not become one. A file that merely talks about `// @manual-file`,
473
+ // this script included, is not opted out.
474
+ const MANUAL_FILE_RE = /^[ \t]*\/\/[ \t]*@manual-file\b/;
475
+ const MANUAL_START_RE = /^[ \t]*\/\/[ \t]*@manual-start\b/;
476
+ const MANUAL_END_RE = /^[ \t]*\/\/[ \t]*@manual-end\b/;
477
+
478
+ /** `// @manual-file` on a line of its own: the generator does not open the file at all. */
479
+ function isManualFile(lines) {
480
+ return lines.some(l => MANUAL_FILE_RE.test(l));
481
+ }
482
+
483
+ /**
484
+ * The line numbers inside `// @manual-start` / `// @manual-end`, the markers included.
485
+ *
486
+ * Recomputed wherever it is needed rather than cached, because every splice moves the lines
487
+ * underneath it. An unclosed start runs to the end of the file, which is the safe reading: the
488
+ * cost of a typo is that the generator declines to edit, never that it edits the wrong thing.
489
+ */
490
+ function manualLines(lines) {
491
+ const out = new Set();
492
+ let open = -1;
493
+ for (let i = 0; i < lines.length; i++) {
494
+ if (MANUAL_START_RE.test(lines[i])) { if (open === -1) open = i; }
495
+ else if (MANUAL_END_RE.test(lines[i]) && open !== -1) {
496
+ for (let j = open; j <= i; j++) out.add(j);
497
+ open = -1;
498
+ }
499
+ }
500
+ if (open !== -1) for (let j = open; j < lines.length; j++) out.add(j);
501
+ return out;
502
+ }
503
+
504
+ /** The first line at or after `at` that is not inside a manual region - where it is safe to insert. */
505
+ function pastManualRegion(lines, at) {
506
+ const manual = manualLines(lines);
507
+ let i = at;
508
+ while (i < lines.length && manual.has(i)) i++;
509
+ return i;
510
+ }
511
+
512
+ // --- the header ------------------------------------------------------------------------------
513
+
514
+ const HEADER_FIRST_RE = new RegExp(`^// @generated-by ${GENERATOR_NAME} v([\\w.\\-]+)`);
515
+
516
+ function renderHeader(scope) {
517
+ return [
518
+ `// @generated-by ${GENERATOR_NAME} v${GENERATOR_VERSION}`,
519
+ ...(scope === 'whole'
520
+ ? ['// This whole file is generated. Put hand-written code in a separate module.']
521
+ : [
522
+ '// Properties of this type in the server metadata are written here and rewritten on',
523
+ '// every run. Everything else in this file is yours and is never touched.',
524
+ '// To keep one of those too, see the manual markers in ./README.md.',
525
+ // ^ deliberately not spelled out. The markers are matched by scanning the file, so a
526
+ // header that named them would opt every generated file out of the generator.
527
+ ]),
528
+ ];
529
+ }
530
+
531
+ /** Whether this file is one the generator has written before. */
532
+ function hasGeneratedHeader(lines) {
533
+ return lines.length > 0 && HEADER_FIRST_RE.test(lines[0]);
534
+ }
535
+
536
+ /**
537
+ * Say what --adopt would do to a hand-written file, and write nothing. The counts are the point:
538
+ * they tell you how much of the class the generator would take over before it does it.
539
+ */
540
+ function reportAdoptable(fileName, className, members) {
541
+ console.log(` skip ${fileName} - no ${GENERATOR_NAME} header, so it is not the generator's`);
542
+ console.log(` ${members.length} propert${members.length === 1 ? 'y' : 'ies'} in the metadata for ${className}`);
543
+ console.log(` --adopt takes it over; --adopt --dry-run shows what that would change`);
544
+ }
545
+
546
+ /** Replace the leading `// @generated-by ...` comment block, or prepend one. Returns [lines, was]. */
547
+ function applyHeader(lines, scope) {
548
+ const header = renderHeader(scope);
549
+ if (lines.length && HEADER_FIRST_RE.test(lines[0])) {
550
+ const wasVersion = HEADER_FIRST_RE.exec(lines[0])[1];
551
+ let end = 1;
552
+ while (end < lines.length && /^\/\//.test(lines[end])) end++;
553
+ return [[...header, ...lines.slice(end)], wasVersion];
554
+ }
555
+ return [[...header, ...lines], null];
556
+ }
557
+
558
+ // --- imports ---------------------------------------------------------------------------------
559
+
560
+ const IMPORT_RE =
561
+ /^import\s+(type\s+)?\{([^}]*)\}\s+from\s+(['"])([^'"]+)\3;?[ \t]*(\/\/.*)?$/;
562
+
563
+ /** What the generated members of one type need to import. */
564
+ function requiredImports(needs, opts, selfName) {
565
+ const required = [];
566
+ if (needs.breeze.size) {
567
+ required.push({
568
+ specifier: opts.breeze,
569
+ names: [...needs.breeze].sort(),
570
+ typeOnly: true,
571
+ });
572
+ }
573
+ // The base class is extended, so it is a value import.
574
+ required.push({ specifier: needs.baseModule, names: [needs.base], typeOnly: false });
575
+
576
+ for (const sibling of [...needs.siblings].sort()) {
577
+ if (sibling === selfName) continue; // a self-reference needs no import
578
+ if (sibling === needs.base) continue; // already imported as a value, above
579
+ required.push({ specifier: `./${kebab(sibling)}${opts.ext}`, names: [sibling], typeOnly: true });
580
+ }
581
+ return required;
582
+ }
583
+
584
+ /**
585
+ * Make sure every required import is present, without removing anything the file added.
586
+ * Marked import lines lose names that are neither required nor referenced anywhere else.
587
+ */
588
+ function reconcileImports(lines, required, notes) {
589
+ const manual = manualLines(lines);
590
+ const statements = [];
591
+ lines.forEach((line, i) => {
592
+ const m = IMPORT_RE.exec(line);
593
+ if (m) {
594
+ statements.push({
595
+ line: i,
596
+ typeOnly: !!m[1],
597
+ names: m[2].split(',').map(s => s.trim()).filter(Boolean),
598
+ specifier: m[4],
599
+ marked: isMarked(line),
600
+ // Protected even when marked `@generated`: a region wins over ownership. The names
601
+ // still count as bound below, so nothing re-imports them.
602
+ manual: isManual(line) || manual.has(i),
603
+ });
604
+ }
605
+ });
606
+
607
+ const requiredSpecifierOf = new Map();
608
+ for (const req of required) {
609
+ for (const name of req.names) requiredSpecifierOf.set(name, req.specifier);
610
+ }
611
+
612
+ // 0. Re-point a marked import whose module has moved - --breeze changed, say, or the classes
613
+ // were regenerated into a different directory. Dropping the name here lets the add step
614
+ // put it back at the right specifier; without this it looks satisfied and the stale module
615
+ // survives. Marked statements only: a hand-written import of the same name is the caller's.
616
+ for (const stmt of statements) {
617
+ if (!stmt.marked || stmt.manual) continue;
618
+ const moved = stmt.names.filter(n => {
619
+ const local = n.split(/\s+as\s+/).pop().trim();
620
+ return requiredSpecifierOf.has(local) && requiredSpecifierOf.get(local) !== stmt.specifier;
621
+ });
622
+ if (!moved.length) continue;
623
+ const to = requiredSpecifierOf.get(moved[0].split(/\s+as\s+/).pop().trim());
624
+ notes.push(`move ${moved.join(', ')} from '${stmt.specifier}' to '${to}'`);
625
+ stmt.names = stmt.names.filter(n => !moved.includes(n));
626
+ lines[stmt.line] = stmt.names.length ? renderImport(stmt) : null;
627
+ }
628
+
629
+ const bound = new Set(statements.flatMap(s => s.names.map(n => n.split(/\s+as\s+/).pop().trim())));
630
+ const requiredBySpecifier = new Map();
631
+ for (const req of required) {
632
+ const key = `${req.specifier}\u0000${req.typeOnly}`;
633
+ const names = requiredBySpecifier.get(key) || [];
634
+ requiredBySpecifier.set(key, names.concat(req.names));
635
+ }
636
+
637
+ // 1. Add what is missing.
638
+ const additions = [];
639
+ for (const [key, names] of requiredBySpecifier) {
640
+ const [specifier, typeOnlyStr] = key.split('\u0000');
641
+ const typeOnly = typeOnlyStr === 'true';
642
+ const missing = names.filter(n => !bound.has(n));
643
+ if (!missing.length) continue;
644
+
645
+ const host = statements.find(s => s.specifier === specifier && s.typeOnly === typeOnly);
646
+ if (host) {
647
+ host.names = [...new Set([...host.names, ...missing])];
648
+ lines[host.line] = renderImport(host);
649
+ notes.push(`import ${missing.join(', ')} from '${specifier}'`);
650
+ } else {
651
+ additions.push({ specifier, typeOnly, names: missing, marked: true });
652
+ notes.push(`import ${missing.join(', ')} from '${specifier}'`);
653
+ }
654
+ missing.forEach(n => bound.add(n));
655
+ }
656
+
657
+ // 2. Prune marked imports that nothing needs any more.
658
+ const requiredNames = new Set(required.flatMap(r => r.names));
659
+ const bodyText = lines.filter(l => l !== null && !IMPORT_RE.test(l)).join('\n');
660
+ for (const stmt of statements) {
661
+ if (!stmt.marked || stmt.manual) continue;
662
+ const keep = stmt.names.filter(n => {
663
+ const local = n.split(/\s+as\s+/).pop().trim();
664
+ if (requiredNames.has(local)) return true;
665
+ return new RegExp(`\\b${local}\\b`).test(bodyText); // still used by hand-written code
666
+ });
667
+ if (keep.length === stmt.names.length) continue;
668
+ const dropped = stmt.names.filter(n => !keep.includes(n));
669
+ notes.push(`drop import ${dropped.join(', ')} from '${stmt.specifier}'`);
670
+ stmt.names = keep;
671
+ lines[stmt.line] = keep.length ? renderImport(stmt) : null;
672
+ }
673
+ // An import statement left with no names - here or in step 0 - is dropped outright. Nulling
674
+ // and filtering once keeps every `stmt.line` index valid until all the passes are done.
675
+ lines = lines.filter(l => l !== null);
676
+
677
+ // 3. Insert the new statements after the last import, or after the header - and never inside
678
+ // a manual region, which is why each insertion point is pushed past one.
679
+ if (additions.length) {
680
+ let at = -1;
681
+ for (let i = 0; i < lines.length; i++) if (IMPORT_RE.test(lines[i])) at = i;
682
+ if (at === -1) {
683
+ at = 0;
684
+ while (at < lines.length && /^\/\//.test(lines[at])) at++;
685
+ at = pastManualRegion(lines, at);
686
+ // Reuse the blank line after the header - left there when a moved import was the only one.
687
+ if (lines[at] === '' && lines[at + 1] === '') {
688
+ at++;
689
+ } else {
690
+ lines.splice(at, 0, '');
691
+ at++;
692
+ }
693
+ lines.splice(at, 0, ...additions.map(renderImport));
694
+ return lines;
695
+ }
696
+ at = pastManualRegion(lines, at + 1) - 1;
697
+ lines.splice(at + 1, 0, ...additions.map(renderImport));
698
+ }
699
+ return lines;
700
+ }
701
+
702
+ function renderImport(stmt) {
703
+ const kind = stmt.typeOnly ? 'import type' : 'import';
704
+ const suffix = stmt.marked ? ` ${MARK}` : '';
705
+ return `${kind} { ${stmt.names.join(', ')} } from '${stmt.specifier}';${suffix}`;
706
+ }
707
+
708
+ // --- properties ------------------------------------------------------------------------------
709
+
710
+ function renderProperty(member, indent) {
711
+ return `${indent}declare ${member.name}: ${member.type}; ${MARK}`;
712
+ }
713
+
714
+ /**
715
+ * Bring the class body's mapped properties into line with `members`, leaving every other member,
716
+ * comment and blank line where it is.
717
+ *
718
+ * **The metadata decides what the generator owns, not the marker.** A declared property whose
719
+ * name is in `members` is a mapped property of this type and is rewritten to the canonical form;
720
+ * a declared property that is not is the caller's and is never touched. That is what lets a
721
+ * hand-written class - which has no markers anywhere - be adopted a property at a time instead of
722
+ * having its whole class body appended a second time.
723
+ *
724
+ * The marker is still written, and is still read for exactly one decision: a declaration the
725
+ * metadata no longer has can be removed only if the generator is the one that put it there. An
726
+ * unmarked property absent from the metadata is a hand-written member and stays.
727
+ *
728
+ * `// @manual` is the way out: it pins a declaration against all of this.
729
+ */
730
+ function reconcileProperties(lines, className, members, notes) {
731
+ const body = findClassBodyLines(lines, className);
732
+ if (!body) fail(`could not find "class ${className}"`);
733
+
734
+ // Index what the class body already declares. `manual` covers both scopes that can protect a
735
+ // single line: the marker on it, and a region enclosing it.
736
+ const manual = manualLines(lines);
737
+ const declared = new Map(); // name -> { line, indent, marked, manual }
738
+ for (let i = body.first; i <= body.last; i++) {
739
+ const d = parseDeclaration(lines[i]);
740
+ if (d) {
741
+ declared.set(d.name, {
742
+ line: i, indent: d.indent, marked: isMarked(lines[i]),
743
+ manual: isManual(lines[i]) || manual.has(i),
744
+ });
745
+ }
746
+ }
747
+
748
+ const wanted = new Map(members.map(m => [m.name, m]));
749
+ const indent = [...declared.values()].find(d => d.marked)?.indent
750
+ ?? [...declared.values()][0]?.indent
751
+ ?? ' ';
752
+
753
+ // 1. Rewrite what is already there, marked or not - the metadata says it is ours.
754
+ const appended = [];
755
+ for (const member of members) {
756
+ const existing = declared.get(member.name);
757
+ if (!existing) { appended.push(member); continue; }
758
+ if (existing.manual) {
759
+ notes.push(`${member.name} is yours (${MANUAL_MARK}) - left as it is`);
760
+ continue;
761
+ }
762
+ // Already says the right thing, however it is spaced - leave the line exactly as it is, so a
763
+ // formatter's pass over the file does not become a change for the generator to undo.
764
+ if (isCanonical(lines[existing.line], member)) continue;
765
+ const next = renderProperty(member, existing.indent);
766
+ if (lines[existing.line] !== next) {
767
+ // Worth distinguishing in the log: the first is routine, the second takes over a line
768
+ // somebody wrote by hand.
769
+ notes.push(existing.marked
770
+ ? `${member.name}: ${member.type}`
771
+ : `adopt ${member.name}: ${member.type}`);
772
+ lines[existing.line] = next;
773
+ }
774
+ }
775
+
776
+ // 2. Drop properties the metadata no longer has - but only ones the generator wrote. This is
777
+ // the single decision the marker still drives: without it, a hand-written member would be
778
+ // indistinguishable from a column that has left the schema.
779
+ const stale = [...declared.entries()]
780
+ .filter(([name, d]) => d.marked && !d.manual && !wanted.has(name))
781
+ .map(([name, d]) => ({ name, line: d.line }));
782
+ for (const { name, line } of stale.sort((a, b) => b.line - a.line)) {
783
+ notes.push(`remove ${name} - no longer in the metadata`);
784
+ lines.splice(line, 1);
785
+ }
786
+
787
+ // 3. Append what is new, after the last mapped property, else at the top of the body.
788
+ //
789
+ // "Mapped" rather than "marked": step 1 has just marked the ones it adopted, so in a file
790
+ // being taken over this lands the new properties with the existing ones instead of above
791
+ // everything - which is where they went when nothing in the class carried a marker.
792
+ if (appended.length) {
793
+ const after = findClassBodyLines(lines, className);
794
+ let at = after.first;
795
+ for (let i = after.first; i <= after.last; i++) {
796
+ const d = parseDeclaration(lines[i]);
797
+ if (d && (isMarked(lines[i]) || wanted.has(d.name))) at = i + 1;
798
+ }
799
+ at = pastManualRegion(lines, at); // never insert into somebody else's block
800
+ for (const member of appended) notes.push(`add ${member.name}: ${member.type}`);
801
+ lines.splice(at, 0, ...appended.map(m => renderProperty(m, indent)));
802
+ }
803
+ return lines;
804
+ }
805
+
806
+ /** The members EntityBase / ComplexObjectBase supply, which a class extending one must not redeclare. */
807
+ const SUPPLIED_MEMBERS = {
808
+ entity: ['entityAspect', 'entityType', 'getProperty', 'setProperty'],
809
+ complex: ['complexAspect', 'complexType', 'getProperty', 'setProperty'],
810
+ };
811
+
812
+ const CLASS_DECL_RE = /^(\s*(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*))([^{]*)\{(.*)$/;
813
+
814
+ /**
815
+ * Make the class extend the base the generator means it to, and stop it redeclaring what that
816
+ * base supplies.
817
+ *
818
+ * Two cases reach here. A hand-written class adopted for the first time typically reads
819
+ * `class Customer implements Entity` and declares `entityAspect`, `entityType`, `getProperty` and
820
+ * `setProperty` itself; extending EntityBase is what makes those - and the generated import of it
821
+ * - mean anything. The second is an existing generated class after `--base` changed, which used
822
+ * to import the new base and go on extending the old one.
823
+ *
824
+ * Only the heritage clause is rewritten. An `implements` of the caller's own is kept, because it
825
+ * says something the generator does not know; `implements Entity` / `implements ComplexObject` is
826
+ * dropped, because the base already implements it.
827
+ */
828
+ function reconcileClassDeclaration(lines, className, needs, isComplexType, notes) {
829
+ const at = lines.findIndex(l => CLASS_DECL_RE.test(l) && CLASS_DECL_RE.exec(l)[2] === className);
830
+ if (at === -1) return lines;
831
+ if (isManual(lines[at]) || manualLines(lines).has(at)) return lines;
832
+
833
+ const [, head, , heritage, tail] = CLASS_DECL_RE.exec(lines[at]);
834
+ const currentExtends = /\bextends\s+([A-Za-z_$][\w$]*)/.exec(heritage);
835
+ if (currentExtends && currentExtends[1] === needs.base) return lines;
836
+
837
+ const supplied = SUPPLIED_MEMBERS[isComplexType ? 'complex' : 'entity'];
838
+ const breezeInterface = isComplexType ? 'ComplexObject' : 'Entity';
839
+ const implemented = (/\bimplements\s+([^{]*)$/.exec(heritage)?.[1] ?? '')
840
+ .split(',').map(s => s.trim()).filter(Boolean)
841
+ .filter(name => name !== breezeInterface);
842
+
843
+ lines[at] = `${head} extends ${needs.base}`
844
+ + (implemented.length ? ` implements ${implemented.join(', ')}` : '')
845
+ + ` {${tail}`;
846
+ notes.push(currentExtends
847
+ ? `extends ${needs.base} - was ${currentExtends[1]}`
848
+ : `extends ${needs.base}`);
849
+
850
+ // Redeclaring what the base supplies shadows it. Without `declare` it is worse than redundant:
851
+ // an ES2022 class field becomes a real own property set to undefined, hiding the accessors
852
+ // Breeze installs on the prototype. See docs/guide/extending-entities.md.
853
+ const body = findClassBodyLines(lines, className);
854
+ if (!body) return lines;
855
+ const manual = manualLines(lines);
856
+ const dropped = [];
857
+ for (let i = body.last; i >= body.first; i--) {
858
+ const d = parseDeclaration(lines[i]);
859
+ if (d && supplied.includes(d.name) && !isManual(lines[i]) && !manual.has(i)) {
860
+ dropped.unshift(d.name);
861
+ lines.splice(i, 1);
862
+ }
863
+ }
864
+ if (dropped.length) {
865
+ notes.push(`remove ${dropped.join(', ')} - supplied by ${needs.base}`);
866
+ // Whatever imported those types is the caller's, and hand-written imports are never removed.
867
+ // Say so rather than leaving a dead import to be discovered by `noUnusedLocals`.
868
+ const stillUsed = lines.filter(l => !IMPORT_RE.test(l)).join('\n');
869
+ const orphaned = lines
870
+ .map(l => IMPORT_RE.exec(l))
871
+ .filter(m => m && !isMarked(m[0]))
872
+ .flatMap(m => m[2].split(',').map(s => s.split(/\s+as\s+/).pop().trim()))
873
+ .filter(n => n && !new RegExp(`\\b${n}\\b`).test(stillUsed));
874
+ if (orphaned.length) {
875
+ notes.push(`${orphaned.join(', ')} may now be unused - imported by hand, so left in place`);
876
+ }
877
+ }
878
+ return lines;
879
+ }
880
+
881
+ // --- rendering a new file ----------------------------------------------------------------------
882
+
883
+ function renderNewFile(stype, members, needs, opts, isComplexType) {
884
+ const kind = isComplexType ? 'complex type' : 'entity type';
885
+ const keys = (stype.keyProperties || []).map(p => p.name).join(', ');
886
+
887
+ const imports = requiredImports(needs, opts, stype.shortName)
888
+ .map(r => renderImport({ ...r, marked: true }));
889
+
890
+ const doc = [
891
+ '/**',
892
+ ` * ${stype.name} - the ${kind}${stype.defaultResourceName ? `, queried as \`${stype.defaultResourceName}\`` : ''}.`,
893
+ ];
894
+ if (keys) doc.push(` * Key: ${keys}.`);
895
+ doc.push(
896
+ ' *',
897
+ ' * Methods, getters and unmapped properties added below survive a regeneration; see',
898
+ ' * ./README.md.',
899
+ ' */'
900
+ );
901
+
902
+ return [
903
+ ...renderHeader('members'),
904
+ '',
905
+ ...imports,
906
+ '',
907
+ ...doc,
908
+ `export class ${stype.shortName} extends ${needs.base} {`,
909
+ ...members.map(m => renderProperty(m, ' ')),
910
+ '}',
911
+ '',
912
+ ].join('\n');
913
+ }
914
+
915
+ // --- the shared base classes and the barrel ----------------------------------------------------
916
+
917
+ /**
918
+ * The class every generated type at the root of its inheritance chain extends, and the module to
919
+ * import it from. `--base` / `--complex-base` name one of the caller's own, so that behaviour can
920
+ * be added to every entity without giving up code generation; without them it is Breeze's own
921
+ * `EntityBase` / `ComplexObjectBase`.
922
+ */
923
+ function rootBase(opts, isComplexType) {
924
+ const custom = isComplexType ? opts.complexBase : opts.base;
925
+ const customModule = isComplexType ? opts.complexBaseModule : opts.baseModule;
926
+ if (!custom) {
927
+ return {
928
+ name: isComplexType ? 'ComplexObjectBase' : 'EntityBase',
929
+ module: opts.breeze,
930
+ file: null,
931
+ isCustom: false,
932
+ isRelative: false,
933
+ isComplexType,
934
+ };
935
+ }
936
+ const module = customModule || `./${kebab(custom)}${opts.ext}`;
937
+ // `file` is set only for a plain neighbour of the generated classes - the one case where the
938
+ // generator knows exactly where to scaffold it.
939
+ let file = null;
940
+ if (module.startsWith('./') && !module.slice(2).includes('/')) {
941
+ const stem = module.slice(2);
942
+ file = (opts.ext && stem.endsWith(opts.ext) ? stem.slice(0, -opts.ext.length) : stem) + '.ts';
943
+ }
944
+ return { name: custom, module, file, isCustom: true, isRelative: module.startsWith('.'), isComplexType };
945
+ }
946
+
947
+ /** A starter for a custom base class, written once if it is not there. Never rewritten. */
948
+ function renderCustomBase(root, opts) {
949
+ const supplied = root.isComplexType ? 'ComplexObjectBase' : 'EntityBase';
950
+ const kind = root.isComplexType ? 'complex object' : 'entity';
951
+ return [
952
+ `// ${root.name} - the base class every generated ${kind} extends.`,
953
+ '//',
954
+ `// ${GENERATOR_NAME} wrote this file once because --${root.isComplexType ? 'complex-base' : 'base'} named a class it`,
955
+ '// could not find. It is yours from here on: nothing in it is marked `@generated` and the',
956
+ '// generator will never rewrite it. Put behaviour every generated class should have here.',
957
+ '//',
958
+ `// Anything added with an initializer becomes an unmapped property on every ${kind}; write`,
959
+ '// methods and getters instead unless that is what you want. See',
960
+ '// docs/guide/extending-entities.md.',
961
+ `import { ${supplied} } from '${opts.breeze}';`,
962
+ '',
963
+ `export abstract class ${root.name} extends ${supplied} {`,
964
+ '}',
965
+ '',
966
+ ].join('\n');
967
+ }
968
+
969
+ /**
970
+ * Before v1.1.0 the generator wrote EntityBase and ComplexObjectBase into entity-base.ts; they now
971
+ * ship in Breeze. A scaffolded custom base imports them from './entity-base', and the generator
972
+ * never rewrites a scaffold, so an entity-base.ts that is already there is kept as a re-export
973
+ * rather than deleted. A model generated fresh never gets one.
974
+ */
975
+ function renderEntityBaseShim(opts) {
976
+ return [
977
+ ...renderHeader('whole'),
978
+ '//',
979
+ `// EntityBase and ComplexObjectBase now ship in ${opts.breeze}. This file re-exports them for`,
980
+ '// code that still imports them from here; once nothing does, delete it and the generator will',
981
+ '// not write it again.',
982
+ `export { ComplexObjectBase, EntityBase } from '${opts.breeze}';`,
983
+ '',
984
+ ].join('\n');
985
+ }
986
+
987
+ function renderIndex(generated, opts) {
988
+ const sorted = [...generated].sort((a, b) => a.shortName.localeCompare(b.shortName));
989
+ const lines = [
990
+ ...renderHeader('whole'),
991
+ '//',
992
+ '// registerModelClasses attaches these classes to one MetadataStore. Breeze binds a class to a',
993
+ '// single store - registering the same class in a second store throws - so call it once, on the',
994
+ '// store the managers under test share.',
995
+ `import type { MetadataStore } from '${opts.breeze}';`,
996
+ `export { ComplexObjectBase, EntityBase } from '${opts.breeze}';`,
997
+ ];
998
+ // A custom base class lives alongside the classes, so the barrel should reach it too.
999
+ for (const root of [rootBase(opts, false), rootBase(opts, true)]) {
1000
+ if (root.isCustom && root.isRelative) {
1001
+ lines.push(`export { ${root.name} } from '${root.module}';`);
1002
+ }
1003
+ }
1004
+ for (const g of sorted) {
1005
+ lines.push(`import { ${g.shortName} } from './${kebab(g.shortName)}${opts.ext}';`);
1006
+ }
1007
+ lines.push(
1008
+ '',
1009
+ 'export {',
1010
+ ...sorted.map(g => ` ${g.shortName},`),
1011
+ '};',
1012
+ '',
1013
+ '/** Every generated class, by the short name Breeze knows it as. */',
1014
+ 'export const modelClasses = {',
1015
+ ...sorted.map(g => ` ${g.shortName},`),
1016
+ '};',
1017
+ '',
1018
+ '/** Register all of them with `metadataStore`. Safe to call twice with the same store. */',
1019
+ 'export function registerModelClasses(metadataStore: MetadataStore) {',
1020
+ ' for (const [name, ctor] of Object.entries(modelClasses)) {',
1021
+ ' metadataStore.registerEntityTypeCtor(name, ctor);',
1022
+ ' }',
1023
+ '}',
1024
+ ''
1025
+ );
1026
+ return lines.join('\n');
1027
+ }
1028
+
1029
+ // --- main --------------------------------------------------------------------------------------
1030
+
1031
+ async function main() {
1032
+ const opts = parseArgs(process.argv.slice(2));
1033
+ const breeze = await loadBreeze();
1034
+ const metadata = await loadMetadata(opts);
1035
+
1036
+ const metadataStore = new breeze.MetadataStore();
1037
+ metadataStore.importMetadata(metadata);
1038
+
1039
+ let stypes = metadataStore.getEntityTypes();
1040
+ if (opts.types) {
1041
+ const wanted = new Set(opts.types);
1042
+ const missing = [...wanted].filter(n => !stypes.some(t => t.shortName === n));
1043
+ if (missing.length) fail(`not in the metadata: ${missing.join(', ')}`);
1044
+ stypes = stypes.filter(t => wanted.has(t.shortName));
1045
+ }
1046
+ if (!stypes.length) fail('the metadata contains no structural types');
1047
+
1048
+ const outDir = resolve(workingDir, opts.out);
1049
+ if (!opts.dryRun) mkdirSync(outDir, { recursive: true });
1050
+
1051
+ console.log(`${GENERATOR_NAME} v${GENERATOR_VERSION}`);
1052
+ console.log(`${stypes.length} types from ${opts.service || opts.metadata} -> ${opts.out}`);
1053
+
1054
+ let changed = 0;
1055
+ const generated = [];
1056
+
1057
+ const write = (name, contents, notes = []) => {
1058
+ const path = join(outDir, name);
1059
+ const before = existsSync(path) ? readFileSync(path, 'utf8') : null;
1060
+ // Everything above renders with \n. Match what the file already uses instead, so a CRLF
1061
+ // checkout is not rewritten to LF on every run - which would also defeat the comparison
1062
+ // below and report every file as edited each time. A new file is left as \n; git applies
1063
+ // whatever the clone's core.autocrlf says.
1064
+ if (before !== null && before.includes('\r\n')) contents = contents.replace(/\r?\n/g, '\r\n');
1065
+ if (before === contents) return;
1066
+ changed++;
1067
+ console.log(` ${before === null ? 'new ' : 'edit'} ${name}`);
1068
+ for (const note of notes) console.log(` ${note}`);
1069
+ if (!opts.dryRun) writeFileSync(path, contents, 'utf8');
1070
+ };
1071
+
1072
+ const entityBasePath = join(outDir, 'entity-base.ts');
1073
+ if (existsSync(entityBasePath) && hasGeneratedHeader(readFileSync(entityBasePath, 'utf8').split(/\r?\n/))) {
1074
+ write('entity-base.ts', renderEntityBaseShim(opts));
1075
+ }
1076
+
1077
+ // A custom base class is the caller's file, so it is scaffolded once and then left alone.
1078
+ for (const root of [rootBase(opts, false), rootBase(opts, true)]) {
1079
+ if (!root.isCustom || !root.file) continue;
1080
+ if (existsSync(join(outDir, root.file))) continue;
1081
+ console.log(` new ${root.file} (yours from here on - the generator never rewrites it)`);
1082
+ if (!opts.dryRun) {
1083
+ writeFileSync(join(outDir, root.file), renderCustomBase(root, opts), 'utf8');
1084
+ }
1085
+ changed++;
1086
+ }
1087
+
1088
+ for (const stype of stypes) {
1089
+ const isComplexType = stype instanceof breeze.ComplexType;
1090
+ const root = rootBase(opts, isComplexType);
1091
+ const needs = {
1092
+ breeze: new Set(),
1093
+ siblings: new Set(),
1094
+ base: root.name,
1095
+ baseModule: root.module,
1096
+ };
1097
+ const members = membersOf(stype, opts, needs);
1098
+
1099
+ // An inheritance chain in the metadata becomes one in the generated classes; only the root
1100
+ // of each chain extends the base class.
1101
+ const baseType = stype.baseEntityType;
1102
+ if (baseType && stypes.includes(baseType)) {
1103
+ needs.base = baseType.shortName;
1104
+ needs.baseModule = `./${kebab(baseType.shortName)}${opts.ext}`;
1105
+ }
1106
+
1107
+ const fileName = `${kebab(stype.shortName)}.ts`;
1108
+ const path = join(outDir, fileName);
1109
+ const notes = [];
1110
+ let contents;
1111
+
1112
+ if (existsSync(path)) {
1113
+ // Split on either ending. A CRLF checkout - which is what git gives a Windows clone by
1114
+ // default - otherwise leaves a trailing \r on every line, and `.` does not match \r in
1115
+ // JavaScript, so the `(\/\/.*)?$` at the end of ANY_DECLARATION_RE stops matching. Every
1116
+ // declared property then reads as "not declared" and the whole class is appended again.
1117
+ // write() puts the file's own endings back.
1118
+ let lines = readFileSync(path, 'utf8').split(/\r?\n/);
1119
+
1120
+ // `// @manual-file` is the caller saying the file is theirs. Nothing below runs, not even
1121
+ // the header stamp - the point of it is that the file comes back byte for byte.
1122
+ if (isManualFile(lines)) {
1123
+ console.log(` keep ${fileName} - ${MANUAL_FILE_MARK}`);
1124
+ generated.push({ shortName: stype.shortName, isComplexType });
1125
+ continue;
1126
+ }
1127
+
1128
+ // A file with no header is one the generator has never written. Taking it over rewrites
1129
+ // declarations somebody typed, so it is reported and skipped unless --adopt says otherwise.
1130
+ if (!hasGeneratedHeader(lines) && !opts.adopt) {
1131
+ reportAdoptable(fileName, stype.shortName, members);
1132
+ generated.push({ shortName: stype.shortName, isComplexType });
1133
+ continue;
1134
+ }
1135
+
1136
+ const [headed, wasVersion] = applyHeader(lines, 'members');
1137
+ lines = headed;
1138
+ if (wasVersion && wasVersion !== GENERATOR_VERSION) {
1139
+ notes.push(`generated by v${wasVersion}, now v${GENERATOR_VERSION}`);
1140
+ } else if (!wasVersion) {
1141
+ notes.push(`adopting a file the generator did not write`);
1142
+ }
1143
+ lines = reconcileClassDeclaration(lines, stype.shortName, needs, isComplexType, notes);
1144
+ lines = reconcileProperties(lines, stype.shortName, members, notes);
1145
+ lines = reconcileImports(lines, requiredImports(needs, opts, stype.shortName), notes);
1146
+ contents = lines.join('\n');
1147
+ } else {
1148
+ contents = renderNewFile(stype, members, needs, opts, isComplexType);
1149
+ }
1150
+ write(fileName, contents, notes);
1151
+ generated.push({ shortName: stype.shortName, isComplexType });
1152
+ }
1153
+
1154
+ if (opts.index) write('index.ts', renderIndex(generated, opts));
1155
+
1156
+ // A file for a type that has left the metadata is reported, never deleted.
1157
+ const known = new Set(['entity-base.ts', 'index.ts',
1158
+ ...[rootBase(opts, false), rootBase(opts, true)]
1159
+ .filter(r => r.isCustom && r.file)
1160
+ .map(r => r.file),
1161
+ ...generated.map(g => `${kebab(g.shortName)}.ts`)]);
1162
+ for (const name of readdirSync(outDir)) {
1163
+ if (/\.ts$/.test(name) && !known.has(name)) {
1164
+ console.log(` ? ${name} has no type in this metadata - delete it by hand if it is stale`);
1165
+ }
1166
+ }
1167
+
1168
+ console.log(opts.dryRun
1169
+ ? `${changed} file(s) would change (dry run)`
1170
+ : `${changed} file(s) written`);
1171
+ }
1172
+
1173
+ main().catch(err => {
1174
+ console.error(err);
1175
+ process.exit(1);
1176
+ });