@postxl/generator 0.74.2 → 1.0.1

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 (189) hide show
  1. package/LICENSE +50 -0
  2. package/README.md +79 -1
  3. package/dist/generator-manager.class.d.ts +59 -0
  4. package/dist/generator-manager.class.js +221 -0
  5. package/dist/generator.class.d.ts +90 -0
  6. package/dist/generator.class.js +32 -0
  7. package/dist/generator.context.d.ts +174 -0
  8. package/dist/generator.context.js +125 -0
  9. package/dist/helpers/branded.types.d.ts +149 -0
  10. package/dist/helpers/branded.types.js +111 -0
  11. package/dist/helpers/config-builder.class.d.ts +27 -0
  12. package/dist/helpers/config-builder.class.js +54 -0
  13. package/dist/helpers/import-generator.class.d.ts +70 -0
  14. package/dist/helpers/import-generator.class.js +166 -0
  15. package/dist/helpers/importable.types.d.ts +52 -0
  16. package/dist/helpers/importable.types.js +15 -0
  17. package/dist/helpers/index-generator.class.d.ts +10 -0
  18. package/dist/helpers/index-generator.class.js +46 -0
  19. package/dist/helpers/index.d.ts +8 -0
  20. package/dist/helpers/index.js +24 -0
  21. package/dist/helpers/package-json.generator.d.ts +56 -0
  22. package/dist/helpers/package-json.generator.js +36 -0
  23. package/dist/helpers/tsconfig.generator.d.ts +1 -0
  24. package/dist/helpers/tsconfig.generator.js +14 -0
  25. package/dist/helpers/verify-context.d.ts +4 -0
  26. package/dist/helpers/verify-context.js +23 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.js +21 -0
  29. package/dist/utils/checksum.d.ts +10 -0
  30. package/dist/utils/checksum.js +132 -0
  31. package/dist/utils/fs-utils.d.ts +34 -0
  32. package/dist/utils/fs-utils.js +126 -0
  33. package/dist/utils/index.d.ts +10 -0
  34. package/dist/utils/index.js +26 -0
  35. package/dist/utils/jsdoc.d.ts +12 -0
  36. package/dist/utils/jsdoc.js +37 -0
  37. package/dist/utils/lint.d.ts +46 -0
  38. package/dist/utils/lint.js +154 -0
  39. package/dist/utils/lockfile.d.ts +7 -0
  40. package/dist/utils/lockfile.js +80 -0
  41. package/dist/utils/logger.class.d.ts +25 -0
  42. package/dist/utils/logger.class.js +55 -0
  43. package/dist/utils/merge-conflict.d.ts +55 -0
  44. package/dist/utils/merge-conflict.js +264 -0
  45. package/dist/utils/path.d.ts +52 -0
  46. package/dist/utils/path.js +183 -0
  47. package/dist/utils/prettier-config.d.ts +2 -0
  48. package/dist/utils/prettier-config.js +13 -0
  49. package/dist/utils/prettier.d.ts +5 -0
  50. package/dist/utils/prettier.js +67 -0
  51. package/dist/utils/prettier.skiptest.d.ts +1 -0
  52. package/dist/utils/prettier.skiptest.js +22 -0
  53. package/dist/utils/promise.d.ts +2 -0
  54. package/dist/utils/promise.js +10 -0
  55. package/dist/utils/string-functions.d.ts +9 -0
  56. package/dist/utils/string-functions.js +23 -0
  57. package/dist/utils/sync-log-result.d.ts +9 -0
  58. package/dist/utils/sync-log-result.js +90 -0
  59. package/dist/utils/sync.d.ts +143 -0
  60. package/dist/utils/sync.js +325 -0
  61. package/dist/utils/template.d.ts +66 -0
  62. package/dist/utils/template.js +159 -0
  63. package/dist/utils/vfs.class.d.ts +115 -0
  64. package/dist/utils/vfs.class.js +239 -0
  65. package/dist/utils/zip.d.ts +13 -0
  66. package/dist/utils/zip.js +40 -0
  67. package/package.json +57 -34
  68. package/dist/generator.d.ts +0 -13
  69. package/dist/generator.js +0 -455
  70. package/dist/generators/enums/react.generator.d.ts +0 -10
  71. package/dist/generators/enums/react.generator.js +0 -110
  72. package/dist/generators/enums/types.generator.d.ts +0 -10
  73. package/dist/generators/enums/types.generator.js +0 -39
  74. package/dist/generators/indices/data/module.generator.d.ts +0 -9
  75. package/dist/generators/indices/data/module.generator.js +0 -60
  76. package/dist/generators/indices/data/service.generator.d.ts +0 -9
  77. package/dist/generators/indices/data/service.generator.js +0 -249
  78. package/dist/generators/indices/data/types.generator.d.ts +0 -9
  79. package/dist/generators/indices/data/types.generator.js +0 -49
  80. package/dist/generators/indices/dispatcher-service.generator.d.ts +0 -9
  81. package/dist/generators/indices/dispatcher-service.generator.js +0 -107
  82. package/dist/generators/indices/export/class.generator.d.ts +0 -9
  83. package/dist/generators/indices/export/class.generator.js +0 -140
  84. package/dist/generators/indices/export/encoder.generator.d.ts +0 -9
  85. package/dist/generators/indices/export/encoder.generator.js +0 -50
  86. package/dist/generators/indices/import/convert-functions.generator.d.ts +0 -9
  87. package/dist/generators/indices/import/convert-functions.generator.js +0 -509
  88. package/dist/generators/indices/import/decoder.generator.d.ts +0 -9
  89. package/dist/generators/indices/import/decoder.generator.js +0 -40
  90. package/dist/generators/indices/import/service.generator.d.ts +0 -9
  91. package/dist/generators/indices/import/service.generator.js +0 -573
  92. package/dist/generators/indices/import/types.generator.d.ts +0 -9
  93. package/dist/generators/indices/import/types.generator.js +0 -242
  94. package/dist/generators/indices/repositories.generator.d.ts +0 -9
  95. package/dist/generators/indices/repositories.generator.js +0 -25
  96. package/dist/generators/indices/routes.generator.d.ts +0 -9
  97. package/dist/generators/indices/routes.generator.js +0 -29
  98. package/dist/generators/indices/seed-migration.generator.d.ts +0 -9
  99. package/dist/generators/indices/seed-migration.generator.js +0 -36
  100. package/dist/generators/indices/seed-template.generator.d.ts +0 -9
  101. package/dist/generators/indices/seed-template.generator.js +0 -80
  102. package/dist/generators/indices/testids.generator.d.ts +0 -7
  103. package/dist/generators/indices/testids.generator.js +0 -71
  104. package/dist/generators/indices/types.generator.d.ts +0 -10
  105. package/dist/generators/indices/types.generator.js +0 -35
  106. package/dist/generators/indices/update/actiontypes.generator.d.ts +0 -9
  107. package/dist/generators/indices/update/actiontypes.generator.js +0 -49
  108. package/dist/generators/indices/update/module.generator.d.ts +0 -9
  109. package/dist/generators/indices/update/module.generator.js +0 -41
  110. package/dist/generators/indices/update/service.generator.d.ts +0 -9
  111. package/dist/generators/indices/update/service.generator.js +0 -34
  112. package/dist/generators/indices/view/module.generator.d.ts +0 -9
  113. package/dist/generators/indices/view/module.generator.js +0 -39
  114. package/dist/generators/indices/view/service.generator.d.ts +0 -9
  115. package/dist/generators/indices/view/service.generator.js +0 -34
  116. package/dist/generators/models/admin.page.generator.d.ts +0 -7
  117. package/dist/generators/models/admin.page.generator.js +0 -74
  118. package/dist/generators/models/export/encoder.generator.d.ts +0 -9
  119. package/dist/generators/models/export/encoder.generator.js +0 -51
  120. package/dist/generators/models/import/decoder.generator.d.ts +0 -9
  121. package/dist/generators/models/import/decoder.generator.js +0 -148
  122. package/dist/generators/models/react/context.generator.d.ts +0 -9
  123. package/dist/generators/models/react/context.generator.js +0 -71
  124. package/dist/generators/models/react/index.d.ts +0 -10
  125. package/dist/generators/models/react/index.js +0 -31
  126. package/dist/generators/models/react/library.generator.d.ts +0 -10
  127. package/dist/generators/models/react/library.generator.js +0 -94
  128. package/dist/generators/models/react/lookup.generator.d.ts +0 -9
  129. package/dist/generators/models/react/lookup.generator.js +0 -175
  130. package/dist/generators/models/react/modals.generator.d.ts +0 -23
  131. package/dist/generators/models/react/modals.generator.js +0 -710
  132. package/dist/generators/models/repository.generator.d.ts +0 -9
  133. package/dist/generators/models/repository.generator.js +0 -955
  134. package/dist/generators/models/route.generator.d.ts +0 -9
  135. package/dist/generators/models/route.generator.js +0 -92
  136. package/dist/generators/models/seed.generator.d.ts +0 -21
  137. package/dist/generators/models/seed.generator.js +0 -285
  138. package/dist/generators/models/stub.generator.d.ts +0 -9
  139. package/dist/generators/models/stub.generator.js +0 -92
  140. package/dist/generators/models/types.generator.d.ts +0 -9
  141. package/dist/generators/models/types.generator.js +0 -125
  142. package/dist/generators/models/update/service.generator.d.ts +0 -10
  143. package/dist/generators/models/update/service.generator.js +0 -302
  144. package/dist/generators/models/view/service.generator.d.ts +0 -10
  145. package/dist/generators/models/view/service.generator.js +0 -239
  146. package/dist/lib/attributes.d.ts +0 -114
  147. package/dist/lib/attributes.js +0 -2
  148. package/dist/lib/exports.d.ts +0 -45
  149. package/dist/lib/exports.js +0 -90
  150. package/dist/lib/imports.d.ts +0 -65
  151. package/dist/lib/imports.js +0 -114
  152. package/dist/lib/meta.d.ts +0 -1191
  153. package/dist/lib/meta.js +0 -434
  154. package/dist/lib/schema/fields.d.ts +0 -46
  155. package/dist/lib/schema/fields.js +0 -62
  156. package/dist/lib/schema/schema.d.ts +0 -466
  157. package/dist/lib/schema/schema.js +0 -18
  158. package/dist/lib/schema/types.d.ts +0 -201
  159. package/dist/lib/schema/types.js +0 -112
  160. package/dist/lib/serializer.d.ts +0 -15
  161. package/dist/lib/serializer.js +0 -24
  162. package/dist/lib/test-id-collector.d.ts +0 -42
  163. package/dist/lib/test-id-collector.js +0 -53
  164. package/dist/lib/types.d.ts +0 -7
  165. package/dist/lib/types.js +0 -13
  166. package/dist/lib/typescript.d.ts +0 -5
  167. package/dist/lib/typescript.js +0 -22
  168. package/dist/lib/utils/ast.d.ts +0 -29
  169. package/dist/lib/utils/ast.js +0 -23
  170. package/dist/lib/utils/error.d.ts +0 -17
  171. package/dist/lib/utils/error.js +0 -52
  172. package/dist/lib/utils/file.d.ts +0 -10
  173. package/dist/lib/utils/file.js +0 -56
  174. package/dist/lib/utils/jsdoc.d.ts +0 -9
  175. package/dist/lib/utils/jsdoc.js +0 -37
  176. package/dist/lib/utils/logger.d.ts +0 -17
  177. package/dist/lib/utils/logger.js +0 -12
  178. package/dist/lib/utils/string.d.ts +0 -40
  179. package/dist/lib/utils/string.js +0 -187
  180. package/dist/lib/utils/types.d.ts +0 -12
  181. package/dist/lib/utils/types.js +0 -2
  182. package/dist/lib/zod.d.ts +0 -8
  183. package/dist/lib/zod.js +0 -60
  184. package/dist/prisma/attributes.d.ts +0 -21
  185. package/dist/prisma/attributes.js +0 -175
  186. package/dist/prisma/client-path.d.ts +0 -7
  187. package/dist/prisma/client-path.js +0 -29
  188. package/dist/prisma/parse.d.ts +0 -12
  189. package/dist/prisma/parse.js +0 -452
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ /**
3
+ * The virtual file system represents a file system that can be manipulated in memory.
4
+ *
5
+ * This sync extension allows a 3-way sync between the file system and the disk.
6
+ *
7
+ * The 3 sources are:
8
+ * 1. The data written by code/the generators to the VFS
9
+ * 2. The hash sums from prior generations (indicating which files were changed)
10
+ * 3. The actual disk files
11
+ *
12
+ * The overall usage pattern is:
13
+ * 1. The VFS initializes with a lock file - and reads the hash sums from the disk
14
+ * 2. The code/generators write to the VFS
15
+ * 3. The VFS "writes" to disk, performing this 3-way sync with the below algorithm
16
+ *
17
+ *
18
+ * ### Algorithm
19
+ *
20
+ * For each file in the VFS we have 3 different states - the file in memory (`virtual`), the hash sum from prior generations from the lock file (`lock`), and the file from disk (`disk`).
21
+ *
22
+ * Each of these 3 states can be "empty" - in case the file was not generated/generated last time/does not exist on disk - or represented by a hash value (`H`).
23
+ *
24
+ * Thus, we have the following potential states for each file:
25
+ * - virtual: `empty` | `Hash1` (hash sum from current generation)
26
+ * - lock: `empty` | `Hash1` | `Hash0` (hash sum from prior generation)
27
+ * - disk: `empty` | `Hash1` | `Hash0` | `HashX` (different hash sum from disk)
28
+ *
29
+ * The below table shows all possible combinations of these states - and the resulting action that needs to be performed:
30
+ *
31
+ * |-------|-------|---------|---------------------------------|---------------------------|
32
+ * | VFS | Lock | Disk | Description | Action on disk |
33
+ * |-------|-------|---------|---------------------------------|---------------------------|
34
+ * | Hash1 | Hash1 | Hash1 | Unchanged | No action |
35
+ * | Hash1 | Hash1 | HashX/0 | Ejected | No action |
36
+ * | Hash1 | Hash1 | empty | Deleted on disk | No action |
37
+ * | Hash1 | Hash0 | Hash1 | Corrupt lock file? | No action |
38
+ * | Hash1 | Hash0 | Hash0 | Changed, not ejected | Update to VFS version |
39
+ * | Hash1 | Hash0 | HashX | Changed, ejected | Create "merge conflict"(2)|
40
+ * | Hash1 | Hash0 | empty | Changed, deleted on disk | Write file (1) |
41
+ * | Hash1 | empty | Hash1 | Corrupt lock file? | No action |
42
+ * | Hash1 | empty | HashX/0 | File ejected, corrupt lock file | Create "merge conflict"(2)|
43
+ * | Hash1 | empty | empty | Newly generated | Write file |
44
+ * |-------|-------|---------|---------------------------------|---------------------------|
45
+ * | empty | Hash0 | Hash0 | Unejected file | Delete file |
46
+ * | empty | Hash0 | HashX | Ejected file was deleted | Delete file (3) |
47
+ * | empty | Hash0 | empty | File was manually deleted first | No action |
48
+ * | empty | empty | HashX | Manual file | No action |
49
+ * | empty | empty | empty | Cannot occur | |
50
+ * |-------|-------|---------|---------------------------------|---------------------------|
51
+ *
52
+ * Notes:
53
+ * (1) In case the file was deleted on disk and changed between the last generation and the current generation, we will write the file to disk. The developer can decide to keep the "restored" file or not. Next time generator runs, it will not be regenerated, as lock file hashes match.
54
+ * (2) In case the file was ejected and changed between the last generation and the current generation, we will overwrite the file on disk - with git "merge conflict" markers. The developer must then decide how to resolve the conflicts.
55
+ * (3) In case the file was deleted by the generator but ejected by the developer, we will delete the file from disk. The developer can decide to revert the deletion or not. Next time generator runs, it will not be regenerated, as lock file hashes match.
56
+ *
57
+ */
58
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
59
+ if (k2 === undefined) k2 = k;
60
+ var desc = Object.getOwnPropertyDescriptor(m, k);
61
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
62
+ desc = { enumerable: true, get: function() { return m[k]; } };
63
+ }
64
+ Object.defineProperty(o, k2, desc);
65
+ }) : (function(o, m, k, k2) {
66
+ if (k2 === undefined) k2 = k;
67
+ o[k2] = m[k];
68
+ }));
69
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
70
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
71
+ }) : function(o, v) {
72
+ o["default"] = v;
73
+ });
74
+ var __importStar = (this && this.__importStar) || (function () {
75
+ var ownKeys = function(o) {
76
+ ownKeys = Object.getOwnPropertyNames || function (o) {
77
+ var ar = [];
78
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
79
+ return ar;
80
+ };
81
+ return ownKeys(o);
82
+ };
83
+ return function (mod) {
84
+ if (mod && mod.__esModule) return mod;
85
+ var result = {};
86
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
87
+ __setModuleDefault(result, mod);
88
+ return result;
89
+ };
90
+ })();
91
+ var __importDefault = (this && this.__importDefault) || function (mod) {
92
+ return (mod && mod.__esModule) ? mod : { "default": mod };
93
+ };
94
+ Object.defineProperty(exports, "__esModule", { value: true });
95
+ exports.sync = sync;
96
+ exports.executeAction = executeAction;
97
+ const promises_1 = __importDefault(require("fs/promises"));
98
+ const p_limit_1 = __importDefault(require("p-limit"));
99
+ const utils_1 = require("@postxl/utils");
100
+ const checksum_1 = require("./checksum");
101
+ const fs_utils_1 = require("./fs-utils");
102
+ const lockfile_1 = require("./lockfile");
103
+ const merge_conflict_1 = require("./merge-conflict");
104
+ const Path = __importStar(require("./path"));
105
+ /**
106
+ * Synchronizes the virtual file system with the disk file system.
107
+ *
108
+ * It takes into account the lock file to determine the state of each file.
109
+ * Depending on the state/checksum of each file in VFS, lock file and disk file system,
110
+ * it will perform the appropriate action (write, delete, merge conflict) to update the disk file system.
111
+ *
112
+ * It also updates the lock file with the current state of the VFS.
113
+ *
114
+ * Note: File filtering is now handled at the VFS level, so only files that were added to the VFS
115
+ * (respecting any file pattern filter) will be synced and tracked in the lock file.
116
+ *
117
+ * IMPORTANT: Before writing any files, this function checks if there are any files with unresolved
118
+ * merge conflict markers. If found, the sync will abort immediately and return an error with the
119
+ * list of files that need to be resolved before generation can continue.
120
+ */
121
+ async function sync({ vfs, lockFilePath, diskFilePath, force }) {
122
+ const diskPathNormalized = Path.normalize(diskFilePath);
123
+ const files = await getFilesStates({ vfs, lockFilePath, diskFilePath });
124
+ // Check for unresolved merge conflicts before writing any files
125
+ const filesWithConflicts = findFilesWithMergeConflicts(files);
126
+ if (filesWithConflicts.length > 0) {
127
+ return {
128
+ success: false,
129
+ error: {
130
+ type: 'UnresolvedMergeConflictsError',
131
+ message: `Cannot proceed with generation: ${filesWithConflicts.length} file(s) contain unresolved merge conflicts. Please resolve these conflicts before running the generator again.`,
132
+ filesWithConflicts,
133
+ },
134
+ };
135
+ }
136
+ const limit = (0, p_limit_1.default)(50);
137
+ const tasks = [];
138
+ const result = {
139
+ errors: [],
140
+ files: {},
141
+ };
142
+ for (const [filePath, inputState] of files) {
143
+ const [action, isEjected] = determineActionState(inputState, force);
144
+ const task = limit(async () => {
145
+ const actionResult = await executeAction(Path.join(diskPathNormalized, filePath), action);
146
+ if (actionResult.isErr()) {
147
+ result.errors.push(actionResult.unwrapErr());
148
+ }
149
+ result.files[filePath] = { action, inputState, actionResult, isEjected };
150
+ });
151
+ tasks.push(task);
152
+ }
153
+ tasks.push(limit(() => (0, lockfile_1.writeLockFile)(lockFilePath, vfs)));
154
+ await Promise.all(tasks);
155
+ return { success: true, ...result };
156
+ }
157
+ /**
158
+ * Finds all files in the disk that contain unresolved merge conflict markers.
159
+ *
160
+ * @param files - The files states map from getFilesStates
161
+ * @returns Array of file paths that contain merge conflict markers
162
+ */
163
+ function findFilesWithMergeConflicts(files) {
164
+ const filesWithConflicts = [];
165
+ for (const [filePath, { disk }] of files) {
166
+ if (disk.state === 'hash' && (0, merge_conflict_1.hasMergeConflictMarkers)(disk.content)) {
167
+ filesWithConflicts.push(filePath);
168
+ }
169
+ }
170
+ return filesWithConflicts;
171
+ }
172
+ function getChecksum(file) {
173
+ switch (file.state) {
174
+ case 'hash':
175
+ return file.hash;
176
+ case 'empty':
177
+ return undefined;
178
+ default:
179
+ throw new utils_1.ExhaustiveSwitchCheck(file);
180
+ }
181
+ }
182
+ function getStateKey({ virtual, lock, disk }) {
183
+ const stateKey_Virtual = virtual.state === 'hash' ? 'V:Hash1' : 'V:empty';
184
+ const H1 = getChecksum(virtual);
185
+ const stateKey_Lock = getStateKey_Lock({ lock, H1 });
186
+ const Hash0 = getChecksum(lock);
187
+ const stateKey_Disk = getStateKey_Disk({ disk, H1, Hash0 });
188
+ return `${stateKey_Virtual}-${stateKey_Lock}-${stateKey_Disk}`;
189
+ }
190
+ function getStateKey_Lock({ lock, H1 }) {
191
+ if (lock.state === 'empty') {
192
+ return 'L:empty';
193
+ }
194
+ if (H1 === undefined) {
195
+ return 'L:Hash0';
196
+ }
197
+ if (lock.hash === H1) {
198
+ return 'L:Hash1';
199
+ }
200
+ return 'L:Hash0';
201
+ }
202
+ function getStateKey_Disk({ disk, H1, Hash0, }) {
203
+ if (disk.state === 'empty') {
204
+ return 'D:empty';
205
+ }
206
+ if (H1 !== undefined && disk.hash === H1) {
207
+ return 'D:Hash1';
208
+ }
209
+ if (Hash0 !== undefined && disk.hash === Hash0) {
210
+ return 'D:Hash0';
211
+ }
212
+ return 'D:HashX';
213
+ }
214
+ async function getDiskFileState(diskFilePath) {
215
+ if (await promises_1.default.stat(diskFilePath).catch(() => null)) {
216
+ const result = await (0, fs_utils_1.readFile)(diskFilePath);
217
+ if (result.isErr()) {
218
+ return { state: 'empty' };
219
+ }
220
+ const content = result.unwrap();
221
+ const checksum = await (0, checksum_1.calculateChecksum)(content);
222
+ return { state: 'hash', hash: checksum, content: content };
223
+ }
224
+ return { state: 'empty' };
225
+ }
226
+ async function getFilesStates({ vfs, lockFilePath, diskFilePath, }) {
227
+ const lockFile = await (0, lockfile_1.readLockFile)(lockFilePath);
228
+ const files = new Map();
229
+ const diskPathNormalized = Path.normalize(diskFilePath);
230
+ // We first add all files from the virtual file system.
231
+ for (const [filePath, content] of vfs.files) {
232
+ const virtual = { state: 'hash', hash: await (0, checksum_1.calculateChecksum)(content), content };
233
+ const path = filePath;
234
+ const lock = lockFile?.has(path) ? { state: 'hash', hash: lockFile.get(path) } : { state: 'empty' };
235
+ const disk = await getDiskFileState(Path.join(diskPathNormalized, path));
236
+ files.set(path, { virtual, lock, disk });
237
+ }
238
+ // Then we add all files from the lock file (as the last generator run might not have created all old files).
239
+ // However, if the VFS has a file pattern filter, we should only consider lock file entries that match the pattern.
240
+ if (lockFile) {
241
+ const virtual = { state: 'empty' };
242
+ for (const [filePath, content] of lockFile) {
243
+ if (files.has(filePath)) {
244
+ continue;
245
+ }
246
+ // Skip files that don't match the VFS pattern filter (if one is set)
247
+ if (!vfs.matchesPattern(filePath)) {
248
+ continue;
249
+ }
250
+ const lock = { state: 'hash', hash: content };
251
+ const disk = await getDiskFileState(Path.join(diskPathNormalized, filePath));
252
+ files.set(filePath, { virtual, lock, disk });
253
+ }
254
+ }
255
+ return files;
256
+ }
257
+ const actionMap = {
258
+ // No change between current and last generator run:
259
+ 'V:Hash1-L:Hash1-D:HashX': ['NoAction', true, 'Write'], // Ejected
260
+ 'V:Hash1-L:Hash1-D:Hash1': ['NoAction', false, 'NoAction'], // Unchanged
261
+ 'V:Hash1-L:Hash1-D:Hash0': ['NoAction', true, 'Write'], // Should not occur, but can be considered as ejected
262
+ 'V:Hash1-L:Hash1-D:empty': ['NoAction', true, 'Write'], // Deleted on disk, we want to keep it this way
263
+ // Change between current and last generator run:
264
+ 'V:Hash1-L:Hash0-D:HashX': ['MergeConflict', true, 'Write'], // File ejected (In case the file was ejected and changed between the last generation and the current generation, we will overwrite the file on disk - with git "merge conflict" markers. The developer must then decide how to resolve the conflicts.)
265
+ 'V:Hash1-L:Hash0-D:Hash1': ['NoAction', false, 'NoAction'], // Should not occur, corrupt lock file?
266
+ 'V:Hash1-L:Hash0-D:Hash0': ['Write', false, 'Write'], // Changed, not ejected
267
+ 'V:Hash1-L:Hash0-D:empty': ['Write', true, 'Write'], // Changed, deleted on disk -> overwrite (in case the file was deleted on disk and changed between the last generation and the current generation, we will write the file to disk. The developer can decide to keep the "restored" file or not. Next time generator runs, it will not be regenerated, as lock file hashes match.)
268
+ // Newly generated, did not exist before:
269
+ 'V:Hash1-L:empty-D:HashX': ['MergeConflict', true, 'Write'], // File ejected or was created manually (In case the file was ejected and changed between the last generation and the current generation, we will overwrite the file on disk - with git "merge conflict" markers. The developer must then decide how to resolve the conflicts.)
270
+ 'V:Hash1-L:empty-D:Hash1': ['NoAction', false, 'NoAction'], // File was manually created first
271
+ 'V:Hash1-L:empty-D:Hash0': ['NoAction', true, 'Write'], // Cannot occur, but kept for type completeness
272
+ 'V:Hash1-L:empty-D:empty': ['Write', false, 'Write'], // Newly generated file
273
+ // Generator did not create the file:
274
+ 'V:empty-L:Hash1-D:HashX': ['NoAction', false, 'NoAction'], // Cannot occur, but kept for type completeness
275
+ 'V:empty-L:Hash1-D:Hash1': ['NoAction', false, 'NoAction'], // Cannot occur, but kept for type completeness
276
+ 'V:empty-L:Hash1-D:Hash0': ['NoAction', false, 'NoAction'], // Cannot occur, but kept for type completeness
277
+ 'V:empty-L:Hash1-D:empty': ['NoAction', false, 'NoAction'], // Cannot occur, but kept for type completeness
278
+ 'V:empty-L:Hash0-D:HashX': ['Delete', true, 'Delete'], // Ejected file was deleted -> delete (in case the file was deleted by the generator but ejected by the developer, we will delete the file from disk. The developer can decide to revert the deletion or not. Next time generator runs, it will not be regenerated, as lock file hashes match.)
279
+ 'V:empty-L:Hash0-D:Hash1': ['Delete', true, 'Delete'], // Indistinguishable from "empty-Hash0-HashX"
280
+ 'V:empty-L:Hash0-D:Hash0': ['Delete', false, 'Delete'], // Unejected file
281
+ 'V:empty-L:Hash0-D:empty': ['NoAction', false, 'NoAction'], // File was manually deleted first
282
+ 'V:empty-L:empty-D:HashX': ['NoAction', false, 'NoAction'], // Manual file
283
+ 'V:empty-L:empty-D:Hash1': ['NoAction', false, 'NoAction'], // Indistinguishable from "empty-empty-HashX"
284
+ 'V:empty-L:empty-D:Hash0': ['NoAction', false, 'NoAction'], // Indistinguishable from "empty-empty-HashX"
285
+ 'V:empty-L:empty-D:empty': ['NoAction', false, 'NoAction'], // Cannot occur
286
+ };
287
+ function determineActionState({ virtual, lock, disk }, force) {
288
+ const stateKey = getStateKey({ virtual, lock, disk });
289
+ const [defaultActionType, isEjected, forceActionType] = actionMap[stateKey];
290
+ const actionType = force ? forceActionType : defaultActionType;
291
+ if (actionType === 'Write') {
292
+ if (virtual.state === 'empty') {
293
+ throw new Error(`This should not happen assuming that the actionMap only has 'Write' actions for entries starting with Hash1, ie a defined virtual file`);
294
+ }
295
+ return [{ type: 'Write', content: virtual.content }, isEjected];
296
+ }
297
+ else if (actionType === 'MergeConflict') {
298
+ if (disk.state === 'empty') {
299
+ throw new Error(`This should not happen assuming that the actionMap only has 'Write' actions for entries starting with H1, ie a defined virtual file`);
300
+ }
301
+ if (virtual.state === 'empty') {
302
+ throw new Error(`This should not happen assuming that the actionMap only has 'Write' actions for entries starting with H1, ie a defined virtual file`);
303
+ }
304
+ return [{ type: 'MergeConflict', diskContent: disk.content, virtualContent: virtual.content }, isEjected];
305
+ }
306
+ return [{ type: actionType }, isEjected];
307
+ }
308
+ async function executeAction(filePath, action) {
309
+ switch (action.type) {
310
+ case 'NoAction':
311
+ return utils_1.Result.ok(undefined);
312
+ case 'Write':
313
+ return (0, fs_utils_1.writeFile)(filePath, action.content);
314
+ case 'MergeConflict':
315
+ return writeMergeConflictFile(filePath, action);
316
+ case 'Delete':
317
+ return (0, fs_utils_1.deleteFile)(filePath);
318
+ default:
319
+ throw new utils_1.ExhaustiveSwitchCheck(action);
320
+ }
321
+ }
322
+ async function writeMergeConflictFile(filePath, { diskContent, virtualContent }) {
323
+ const content = (0, merge_conflict_1.generateMergeConflict)({ contentSource: diskContent, contentIncoming: virtualContent });
324
+ return (0, fs_utils_1.writeFile)(filePath, content);
325
+ }
@@ -0,0 +1,66 @@
1
+ import { Result } from '@postxl/utils';
2
+ /**
3
+ * Error thrown when a template variable is not found in the context.
4
+ */
5
+ export declare class TemplateVariableError extends Error {
6
+ readonly variablePath: string;
7
+ readonly availableKeys: string[];
8
+ constructor(variablePath: string, availableKeys: string[]);
9
+ }
10
+ export type RenderTemplateOptions = {
11
+ /**
12
+ * If true, undefined variables will be replaced with an empty string.
13
+ * If false (default), an error will be thrown for undefined variables.
14
+ */
15
+ allowUndefined?: boolean;
16
+ };
17
+ /**
18
+ * Renders a template string by replacing `<%expression%>` placeholders with values from the context.
19
+ *
20
+ * The expression supports dot notation for accessing nested properties.
21
+ *
22
+ * @example
23
+ * renderTemplate('Hello <%name%>!', { name: 'World' })
24
+ * // returns 'Hello World!'
25
+ *
26
+ * @example
27
+ * renderTemplate('Project: <%schema.slug%>', { schema: { slug: 'demo' } })
28
+ * // returns 'Project: demo'
29
+ *
30
+ * @param template - The template string containing `<%expression%>` placeholders
31
+ * @param context - The context object containing values to substitute
32
+ * @param options - Optional configuration
33
+ * @returns The rendered string with all placeholders replaced
34
+ * @throws {TemplateVariableError} If a variable is not found and allowUndefined is false
35
+ */
36
+ export declare function renderTemplate(template: string, context: Record<string, unknown>, options?: RenderTemplateOptions): string;
37
+ type ReadTemplateError = {
38
+ type: 'ReadTemplateError';
39
+ message: string;
40
+ error: Error;
41
+ };
42
+ /**
43
+ * Reads a template file from disk and renders it with the given context.
44
+ *
45
+ * This is a convenience function for loading static template files and
46
+ * replacing placeholders in a single operation.
47
+ *
48
+ * @example
49
+ * // In a generator:
50
+ * const content = await generateFromTemplate({
51
+ * file: path.resolve(__dirname, './template/scripts/docker.sh'),
52
+ * context: { schema: context.schema }
53
+ * })
54
+ * vfs.write('/scripts/docker.sh', content)
55
+ *
56
+ * @param params.file - Absolute path to the template file on disk
57
+ * @param params.context - The context object containing values to substitute
58
+ * @param params.options - Optional render options
59
+ * @returns The rendered template content
60
+ */
61
+ export declare function generateFromTemplate({ file, context, options, }: {
62
+ file: string;
63
+ context: Record<string, unknown>;
64
+ options?: RenderTemplateOptions;
65
+ }): Promise<Result<string, ReadTemplateError>>;
66
+ export {};
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TemplateVariableError = void 0;
37
+ exports.renderTemplate = renderTemplate;
38
+ exports.generateFromTemplate = generateFromTemplate;
39
+ const fs = __importStar(require("node:fs/promises"));
40
+ const utils_1 = require("@postxl/utils");
41
+ const Path = __importStar(require("./path"));
42
+ /**
43
+ * Template delimiter configuration.
44
+ * Uses ERB-style `<%...%>` syntax which doesn't interfere with:
45
+ * - TypeScript/JavaScript template literals (`${...}`)
46
+ * - JSX/TSX syntax
47
+ * - Markdown syntax
48
+ * - Bash variables (`$VAR`, `${VAR}`)
49
+ * - Bash conditionals (`[[ ... ]]`)
50
+ */
51
+ const TEMPLATE_REGEX = /<%\s*([^%]+?)\s*%>/g;
52
+ /**
53
+ * Gets a nested property value from an object using dot notation.
54
+ *
55
+ * @example
56
+ * getNestedValue({ schema: { slug: 'demo' } }, 'schema.slug') // returns 'demo'
57
+ * getNestedValue({ name: 'test' }, 'name') // returns 'test'
58
+ */
59
+ function getNestedValue(obj, path) {
60
+ const keys = path.split('.');
61
+ let current = obj;
62
+ for (const key of keys) {
63
+ if (current === null || current === undefined) {
64
+ return undefined;
65
+ }
66
+ if (typeof current !== 'object') {
67
+ return undefined;
68
+ }
69
+ current = current[key];
70
+ }
71
+ return current;
72
+ }
73
+ /**
74
+ * Error thrown when a template variable is not found in the context.
75
+ */
76
+ class TemplateVariableError extends Error {
77
+ variablePath;
78
+ availableKeys;
79
+ constructor(variablePath, availableKeys) {
80
+ super(`Template variable "<%${variablePath}%>" not found in context. Available top-level keys: ${availableKeys.join(', ')}`);
81
+ this.variablePath = variablePath;
82
+ this.availableKeys = availableKeys;
83
+ this.name = 'TemplateVariableError';
84
+ }
85
+ }
86
+ exports.TemplateVariableError = TemplateVariableError;
87
+ /**
88
+ * Renders a template string by replacing `<%expression%>` placeholders with values from the context.
89
+ *
90
+ * The expression supports dot notation for accessing nested properties.
91
+ *
92
+ * @example
93
+ * renderTemplate('Hello <%name%>!', { name: 'World' })
94
+ * // returns 'Hello World!'
95
+ *
96
+ * @example
97
+ * renderTemplate('Project: <%schema.slug%>', { schema: { slug: 'demo' } })
98
+ * // returns 'Project: demo'
99
+ *
100
+ * @param template - The template string containing `<%expression%>` placeholders
101
+ * @param context - The context object containing values to substitute
102
+ * @param options - Optional configuration
103
+ * @returns The rendered string with all placeholders replaced
104
+ * @throws {TemplateVariableError} If a variable is not found and allowUndefined is false
105
+ */
106
+ function renderTemplate(template, context, options = {}) {
107
+ const { allowUndefined = false } = options;
108
+ return template.replaceAll(TEMPLATE_REGEX, (_match, expression) => {
109
+ const trimmedExpr = expression.trim();
110
+ const value = getNestedValue(context, trimmedExpr);
111
+ if (value === undefined) {
112
+ if (allowUndefined) {
113
+ return '';
114
+ }
115
+ throw new TemplateVariableError(trimmedExpr, Object.keys(context));
116
+ }
117
+ // Convert value to string
118
+ if (typeof value === 'string') {
119
+ return value;
120
+ }
121
+ if (typeof value === 'number' || typeof value === 'boolean') {
122
+ return String(value);
123
+ }
124
+ // For objects/arrays, stringify them
125
+ return JSON.stringify(value);
126
+ });
127
+ }
128
+ /**
129
+ * Reads a template file from disk and renders it with the given context.
130
+ *
131
+ * This is a convenience function for loading static template files and
132
+ * replacing placeholders in a single operation.
133
+ *
134
+ * @example
135
+ * // In a generator:
136
+ * const content = await generateFromTemplate({
137
+ * file: path.resolve(__dirname, './template/scripts/docker.sh'),
138
+ * context: { schema: context.schema }
139
+ * })
140
+ * vfs.write('/scripts/docker.sh', content)
141
+ *
142
+ * @param params.file - Absolute path to the template file on disk
143
+ * @param params.context - The context object containing values to substitute
144
+ * @param params.options - Optional render options
145
+ * @returns The rendered template content
146
+ */
147
+ async function generateFromTemplate({ file, context, options, }) {
148
+ const normalizedPath = Path.normalize(file);
149
+ const result = await utils_1.Result.fromPromise(() => fs.readFile(normalizedPath, { encoding: 'utf-8' }), (error) => ({
150
+ type: 'ReadTemplateError',
151
+ message: `Error reading template file "${file}"`,
152
+ error: error,
153
+ }));
154
+ if (result.isErr()) {
155
+ return result;
156
+ }
157
+ const rendered = renderTemplate(result.unwrap(), context, options);
158
+ return utils_1.Result.ok(rendered);
159
+ }
@@ -0,0 +1,115 @@
1
+ import { FileContent } from './fs-utils';
2
+ import * as Path from './path';
3
+ /**
4
+ * Options for configuring the VirtualFileSystem.
5
+ */
6
+ export type VFSOptions = {
7
+ /**
8
+ * Optional glob pattern to filter files.
9
+ * Examples: `**\/*.ts` or `backend/libs/**`
10
+ * When specified, only files matching this pattern will be stored in the VFS.
11
+ */
12
+ filePattern?: string;
13
+ };
14
+ /**
15
+ * The virtual file system (VFS) represents a file system that can be manipulated in memory.
16
+ *
17
+ * It exposes the following methods:
18
+ * - `write(path, content)`: Writes the given content to the given path
19
+ * - `read(path)`: Reads the content of the given path
20
+ * - `insertFromVfs(path, vfs)`: Inserts the content of the given VFS into the current VFS
21
+ * - `insertFromDisk(path, disk)`: Inserts the content of the given disk into the current VFS
22
+ * - `transform(fn, filter?)`: Transforms the content of all files in the VFS using the given function
23
+ *
24
+ * ## Implementation details
25
+ *
26
+ * All paths are relative to the root of the VFS - and are normalized to use the POSIX path separator.
27
+ * For this we use the `path` utils (path.normalize, path.join, path.parse). All paths are represented
28
+ * as branded strings (`Path.PosixPath`).
29
+ *
30
+ * The file content can either be a (UTF8) string or a Buffer.
31
+ *
32
+ * ## File Pattern Filtering
33
+ *
34
+ * The VFS can be configured with an optional file pattern filter during construction.
35
+ * When a pattern is set, only files matching the pattern will be stored in the VFS.
36
+ * This optimization ensures that linting, formatting, and lock file updates only affect
37
+ * the filtered files, significantly improving performance.
38
+ */
39
+ export declare class VirtualFileSystem {
40
+ #private;
41
+ /**
42
+ * Constructs a new VirtualFileSystem.
43
+ *
44
+ * @param options - Optional configuration options for the VFS.
45
+ */
46
+ constructor(options?: VFSOptions);
47
+ /**
48
+ * Returns all file names in the VFS.
49
+ */
50
+ get fileNames(): Path.PosixPath[];
51
+ /**
52
+ * Returns all files in the VFS.
53
+ */
54
+ get files(): Map<Path.PosixPath | string, FileContent>;
55
+ /**
56
+ * Returns the file pattern filter if one is set.
57
+ */
58
+ get filePattern(): string | undefined;
59
+ /**
60
+ * Checks if a file path matches the VFS file pattern (if one is set).
61
+ * Returns true if no pattern is set, or if the path matches the pattern.
62
+ */
63
+ matchesPattern(filePath: Path.PosixPath | string): boolean;
64
+ /**
65
+ * Writes the given content to the specified path.
66
+ * If a file pattern is set, only files matching the pattern will be stored.
67
+ */
68
+ write(path: string, content: FileContent): void;
69
+ /**
70
+ * Reads the content of a file from the specified path.
71
+ */
72
+ get(path: string): FileContent | undefined;
73
+ /**
74
+ * Reads the content of the specified folder.
75
+ */
76
+ getFolder(path: string): VirtualFileSystem | undefined;
77
+ /**
78
+ * Inserts the content of another VFS into this VFS at the specified path.
79
+ */
80
+ insertFromVfs({ vfs, targetPath }: {
81
+ vfs: VirtualFileSystem;
82
+ targetPath?: string;
83
+ }): void;
84
+ /**
85
+ * Loads the content from the folder on disk into this VFS at the specified path.
86
+ */
87
+ loadFolder({ diskPath, targetPath, recursive, filter, }: {
88
+ diskPath: string;
89
+ targetPath?: string;
90
+ filter?: (path: Path.PosixPath) => boolean;
91
+ recursive?: boolean;
92
+ }): Promise<void>;
93
+ /**
94
+ * Loads the content from the file on disk into this VFS at the specified path.
95
+ */
96
+ loadFile({ diskPath, targetPath }: {
97
+ diskPath: string;
98
+ targetPath?: string;
99
+ }): Promise<void>;
100
+ /**
101
+ * Transforms the content of all files using the provided function.
102
+ * An optional filter function can be provided to select specific files.
103
+ */
104
+ transform(fn: (params: {
105
+ content: FileContent;
106
+ path: Path.PosixPath;
107
+ }) => Promise<FileContent>, { filter, onError, }?: {
108
+ filter?: (path: Path.PosixPath) => boolean;
109
+ onError?: (data: {
110
+ path: Path.PosixPath;
111
+ content: FileContent;
112
+ error: Error;
113
+ }) => void;
114
+ }): Promise<void>;
115
+ }