@postxl/generator 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/dist/helpers/branded.types.d.ts +29 -29
- package/dist/helpers/branded.types.js +3 -1
- package/dist/utils/checksum.d.ts +1 -1
- package/dist/utils/custom-blocks.d.ts +116 -0
- package/dist/utils/custom-blocks.js +454 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/lockfile.d.ts +1 -1
- package/dist/utils/merge-conflict.d.ts +12 -0
- package/dist/utils/merge-conflict.js +127 -2
- package/dist/utils/path.d.ts +2 -2
- package/dist/utils/string-functions.d.ts +2 -2
- package/dist/utils/sync-log-result.js +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -79,3 +79,281 @@ This file contains the hash values for each generated file. With this hash value
|
|
|
79
79
|
the hash in the lock file
|
|
80
80
|
- If the file was changed by the latest generator run: in this case the hash of the newly generated
|
|
81
81
|
file will be different from the hash in the lock file
|
|
82
|
+
|
|
83
|
+
## File Sync Algorithm
|
|
84
|
+
|
|
85
|
+
The generator uses a **3-way sync algorithm** to intelligently handle file changes. The three sources are:
|
|
86
|
+
|
|
87
|
+
1. **Virtual File System (VFS)** - The newly generated content
|
|
88
|
+
2. **Lock File** - Hash values from the previous generation run (`postxl-lock.json`)
|
|
89
|
+
3. **Disk** - The actual files on the filesystem
|
|
90
|
+
|
|
91
|
+
### State Matrix
|
|
92
|
+
|
|
93
|
+
| Generated | Lock File | Disk | Action | Description |
|
|
94
|
+
| --------- | --------- | -------- | ------------------ | -------------------------------------------------------- |
|
|
95
|
+
| ✓ Changed | Same | Modified | **Merge Conflict** | File was ejected AND generator template changed |
|
|
96
|
+
| ✓ Changed | Same | Same | Write | Template changed, file not ejected → auto-update |
|
|
97
|
+
| ✓ Same | Same | Modified | No Action | File ejected, but template unchanged → keep your changes |
|
|
98
|
+
| ✓ Same | Same | Same | No Action | Nothing changed |
|
|
99
|
+
| ✓ New | - | Exists | **Merge Conflict** | New generated file conflicts with existing file |
|
|
100
|
+
| ✓ New | - | - | Write | Brand new file |
|
|
101
|
+
| - Removed | Exists | Modified | Delete | Generator no longer produces this file |
|
|
102
|
+
|
|
103
|
+
### Ejected Files
|
|
104
|
+
|
|
105
|
+
A file is considered **"ejected"** when you manually modify it. Once ejected:
|
|
106
|
+
|
|
107
|
+
- The generator will not overwrite your changes automatically
|
|
108
|
+
- If the generator template changes, you'll get a merge conflict to resolve
|
|
109
|
+
- The file remains tracked in `postxl-lock.json` so the generator knows it exists
|
|
110
|
+
|
|
111
|
+
## Merge Conflicts
|
|
112
|
+
|
|
113
|
+
When both you and the generator have made changes to the same file, the generator creates Git-style merge conflict markers:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Unchanged code stays clean
|
|
117
|
+
import { Injectable } from '@nestjs/common'
|
|
118
|
+
|
|
119
|
+
@Injectable()
|
|
120
|
+
export class UserService {
|
|
121
|
+
<<<<<<< Manual
|
|
122
|
+
// Your manual changes appear here
|
|
123
|
+
findAll() {
|
|
124
|
+
return this.customLogic()
|
|
125
|
+
}
|
|
126
|
+
=======
|
|
127
|
+
// Generated version appears here
|
|
128
|
+
findAll() {
|
|
129
|
+
return this.repository.findAll()
|
|
130
|
+
}
|
|
131
|
+
>>>>>>> Generated
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Resolving Merge Conflicts
|
|
136
|
+
|
|
137
|
+
1. Open the file in your editor
|
|
138
|
+
2. Decide which version to keep (or combine both)
|
|
139
|
+
3. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`)
|
|
140
|
+
4. Run the generator again to verify
|
|
141
|
+
|
|
142
|
+
> **Note**: The generator will refuse to run if there are unresolved merge conflicts in your project (unless `--force` flag is set). Resolve all conflicts before regenerating.
|
|
143
|
+
|
|
144
|
+
### Force Regeneration
|
|
145
|
+
|
|
146
|
+
If you want to discard your changes and reset to the generated version:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Force regenerate a specific file
|
|
150
|
+
pnpm run generate -f -p 'backend/libs/types/**/*.ts'
|
|
151
|
+
|
|
152
|
+
# Force regenerate everything (careful!)
|
|
153
|
+
pnpm run generate -f
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Custom Block Preservation
|
|
157
|
+
|
|
158
|
+
When you extend generated files with custom code, you can mark your additions with special comment markers. This prevents unnecessary merge conflicts when the generator updates other parts of the file.
|
|
159
|
+
|
|
160
|
+
### Basic Usage
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { Injectable } from '@nestjs/common'
|
|
164
|
+
|
|
165
|
+
// @custom-start:imports
|
|
166
|
+
import { CustomLogger } from './logger'
|
|
167
|
+
import { MetricsService } from './metrics'
|
|
168
|
+
// @custom-end:imports
|
|
169
|
+
|
|
170
|
+
@Injectable()
|
|
171
|
+
export class UserService {
|
|
172
|
+
constructor(
|
|
173
|
+
private readonly repository: UserRepository,
|
|
174
|
+
// @custom-start:dependencies
|
|
175
|
+
private readonly logger: CustomLogger,
|
|
176
|
+
private readonly metrics: MetricsService,
|
|
177
|
+
// @custom-end:dependencies
|
|
178
|
+
) {}
|
|
179
|
+
|
|
180
|
+
// Generated methods...
|
|
181
|
+
findAll() {
|
|
182
|
+
return this.repository.findAll()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// @custom-start:customMethods
|
|
186
|
+
async findAllWithMetrics() {
|
|
187
|
+
this.metrics.increment('user.findAll')
|
|
188
|
+
return this.findAll()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async customBusinessLogic() {
|
|
192
|
+
this.logger.log('Custom logic executed')
|
|
193
|
+
return 'custom result'
|
|
194
|
+
}
|
|
195
|
+
// @custom-end:customMethods
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### How It Works
|
|
200
|
+
|
|
201
|
+
When the generator runs and detects custom block markers in an ejected file:
|
|
202
|
+
|
|
203
|
+
1. **Extract**: Custom blocks are identified and extracted from your modified file
|
|
204
|
+
2. **Compare**: The remaining code (minus custom blocks) is compared to the new generated output
|
|
205
|
+
3. **Reinsert**: Custom blocks are automatically inserted back into the generated output at the same relative position
|
|
206
|
+
4. **Conflict only if needed**: Only actual changes outside your custom blocks will show merge conflict markers
|
|
207
|
+
|
|
208
|
+
### Marker Syntax
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Line comment style (recommended)
|
|
212
|
+
// @custom-start:blockName
|
|
213
|
+
// ... your custom code ...
|
|
214
|
+
// @custom-end:blockName
|
|
215
|
+
|
|
216
|
+
// Unnamed blocks (works, but names help with clarity)
|
|
217
|
+
// @custom-start
|
|
218
|
+
// ... your custom code ...
|
|
219
|
+
// @custom-end
|
|
220
|
+
|
|
221
|
+
// Block comment style (for languages that prefer it)
|
|
222
|
+
/* @custom-start:blockName */
|
|
223
|
+
/* ... your custom code ... */
|
|
224
|
+
/* @custom-end:blockName */
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Block Names
|
|
228
|
+
|
|
229
|
+
Names are optional but recommended when you have multiple custom blocks:
|
|
230
|
+
|
|
231
|
+
- Must be alphanumeric with hyphens/underscores: `[a-zA-Z0-9_-]+`
|
|
232
|
+
- Help identify blocks in warnings
|
|
233
|
+
- Opening and closing names should match
|
|
234
|
+
|
|
235
|
+
### Anchor-Based Positioning
|
|
236
|
+
|
|
237
|
+
Custom blocks are repositioned based on **anchor context** - the significant code lines immediately before and after your block. For best results:
|
|
238
|
+
|
|
239
|
+
- Place custom blocks after stable, identifiable lines (method signatures, class declarations, import statements)
|
|
240
|
+
- Avoid placing blocks in areas that frequently change
|
|
241
|
+
- The more unique the surrounding context, the more reliable the repositioning
|
|
242
|
+
|
|
243
|
+
### When Blocks Cannot Be Placed
|
|
244
|
+
|
|
245
|
+
If the generator cannot find a suitable position for a custom block (e.g., the surrounding code changed significantly), it will:
|
|
246
|
+
|
|
247
|
+
1. Append the block at the end of the file
|
|
248
|
+
2. Add a warning comment so you know to move it manually
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// ... rest of file ...
|
|
252
|
+
|
|
253
|
+
// ⚠️ WARNING: The following custom blocks could not be automatically placed.
|
|
254
|
+
// Please manually move them to the appropriate location.
|
|
255
|
+
|
|
256
|
+
// --- Unplaced custom block: orphanedFeature ---
|
|
257
|
+
// @custom-start:orphanedFeature
|
|
258
|
+
// This code needs to be moved manually
|
|
259
|
+
// @custom-end:orphanedFeature
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Best Practices
|
|
263
|
+
|
|
264
|
+
1. **Use descriptive names**: `// @custom-start:authMiddleware` is better than `// @custom-start`
|
|
265
|
+
2. **Keep blocks focused**: One feature per block makes them easier to manage
|
|
266
|
+
3. **Place strategically**: Put blocks after stable anchor points
|
|
267
|
+
4. **Don't nest blocks**: Nested custom blocks are not supported
|
|
268
|
+
5. **Match names**: Ensure `@custom-start:foo` has a matching `@custom-end:foo`
|
|
269
|
+
|
|
270
|
+
### Example: Adding Custom Routes
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// Generated router file
|
|
274
|
+
import { Router } from 'express'
|
|
275
|
+
import { getUsers, getUserById, createUser } from './handlers'
|
|
276
|
+
|
|
277
|
+
const router = Router()
|
|
278
|
+
|
|
279
|
+
// Generated routes
|
|
280
|
+
router.get('/users', getUsers)
|
|
281
|
+
router.get('/users/:id', getUserById)
|
|
282
|
+
router.post('/users', createUser)
|
|
283
|
+
|
|
284
|
+
// @custom-start:customRoutes
|
|
285
|
+
// Custom export endpoint
|
|
286
|
+
router.get('/users/export', async (req, res) => {
|
|
287
|
+
const users = await exportUsersToCSV()
|
|
288
|
+
res.attachment('users.csv').send(users)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// Custom bulk operations
|
|
292
|
+
router.post('/users/bulk', bulkCreateUsers)
|
|
293
|
+
router.delete('/users/bulk', bulkDeleteUsers)
|
|
294
|
+
// @custom-end:customRoutes
|
|
295
|
+
|
|
296
|
+
export default router
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
When the generator adds new routes, your custom routes will be preserved without conflict markers (assuming the anchor context—the generated routes above—remains recognizable).
|
|
300
|
+
|
|
301
|
+
## CLI Options
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
# Standard generation
|
|
305
|
+
pnpm run generate
|
|
306
|
+
|
|
307
|
+
# Force regenerate all files (overwrites ejected files)
|
|
308
|
+
pnpm run generate -f
|
|
309
|
+
|
|
310
|
+
# Force regenerate specific files (glob pattern)
|
|
311
|
+
pnpm run generate -f -p 'backend/libs/types/**/*.ts'
|
|
312
|
+
|
|
313
|
+
# Show ejected files after generation
|
|
314
|
+
pnpm run generate -e
|
|
315
|
+
|
|
316
|
+
# Show diff between ejected and generated versions
|
|
317
|
+
pnpm run generate -d
|
|
318
|
+
|
|
319
|
+
# Watch mode - regenerate on schema changes
|
|
320
|
+
pnpm run generate:watch
|
|
321
|
+
|
|
322
|
+
# Skip linting and formatting
|
|
323
|
+
pnpm run generate -t
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Troubleshooting
|
|
327
|
+
|
|
328
|
+
### "Unresolved merge conflicts detected"
|
|
329
|
+
|
|
330
|
+
The generator found files with conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). Resolve these manually before running the generator again.
|
|
331
|
+
|
|
332
|
+
### Custom blocks appearing at end of file
|
|
333
|
+
|
|
334
|
+
The generator couldn't find the anchor context for your block. This happens when:
|
|
335
|
+
|
|
336
|
+
- The code before/after your block changed significantly
|
|
337
|
+
- The block was placed in a frequently-changing area
|
|
338
|
+
|
|
339
|
+
Solution: Move the block back to its correct position and ensure it has stable anchor lines nearby.
|
|
340
|
+
|
|
341
|
+
### Unexpected merge conflicts in custom block areas
|
|
342
|
+
|
|
343
|
+
If you're seeing conflicts around custom blocks, check:
|
|
344
|
+
|
|
345
|
+
- Block markers are properly formatted (`@custom-start`/`@custom-end`)
|
|
346
|
+
- Names match between start and end markers
|
|
347
|
+
- No nested custom blocks
|
|
348
|
+
|
|
349
|
+
### Lock file out of sync
|
|
350
|
+
|
|
351
|
+
If `postxl-lock.json` gets out of sync with your files:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
# Regenerate everything (preserves ejected files unless they conflict)
|
|
355
|
+
pnpm run generate
|
|
356
|
+
|
|
357
|
+
# Or force regenerate to reset lock file
|
|
358
|
+
pnpm run generate -f
|
|
359
|
+
```
|
|
@@ -31,18 +31,18 @@
|
|
|
31
31
|
* - `ImportPaths`: FilePath | PackageName
|
|
32
32
|
*/
|
|
33
33
|
import z from 'zod';
|
|
34
|
-
declare const zGeneratorInterfaceId: z.ZodBranded<z.ZodString, "PXL.GeneratorInterfaceId">;
|
|
34
|
+
declare const zGeneratorInterfaceId: z.core.$ZodBranded<z.ZodString, "PXL.GeneratorInterfaceId", "out">;
|
|
35
35
|
/**
|
|
36
36
|
* A generator interface id that is used identify what interface a generator implements.
|
|
37
37
|
*/
|
|
38
38
|
export type GeneratorInterfaceId = z.infer<typeof zGeneratorInterfaceId>;
|
|
39
|
-
export declare const toGeneratorInterfaceId: (input: string) => string & z.
|
|
40
|
-
declare const zTypeName: z.ZodBranded<z.ZodBranded<z.ZodString, "PXL.TypeName">, "PXL.Importable">;
|
|
39
|
+
export declare const toGeneratorInterfaceId: (input: string) => string & z.core.$brand<"PXL.GeneratorInterfaceId">;
|
|
40
|
+
declare const zTypeName: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "PXL.TypeName", "out">, "PXL.Importable", "out">;
|
|
41
41
|
/**
|
|
42
42
|
* A type name that is used to refer to a type in the generated code.
|
|
43
43
|
*/
|
|
44
44
|
export type TypeName = z.infer<typeof zTypeName>;
|
|
45
|
-
export declare const toTypeName: (input: string) => string & z.
|
|
45
|
+
export declare const toTypeName: (input: string) => string & z.core.$brand<"PXL.TypeName"> & z.core.$brand<"PXL.Importable">;
|
|
46
46
|
/**
|
|
47
47
|
* A TypeName that is annotated with the kind of the type.
|
|
48
48
|
* Note: This is used when distinguishing between kinds is required at runtime.
|
|
@@ -59,80 +59,80 @@ export declare const toAnnotatedTypeName: (name: TypeName) => AnnotatedTypeName;
|
|
|
59
59
|
* Type guard to check if a given type is an AnnotatedTypeName.
|
|
60
60
|
*/
|
|
61
61
|
export declare const isAnnotatedTypeName: (t: string | AnnotatedTypeName) => t is AnnotatedTypeName;
|
|
62
|
-
declare const zFunctionName: z.ZodBranded<z.ZodBranded<z.ZodString, "PXL.FunctionName">, "PXL.Importable">;
|
|
62
|
+
declare const zFunctionName: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "PXL.FunctionName", "out">, "PXL.Importable", "out">;
|
|
63
63
|
/**
|
|
64
64
|
* A function name that is used to refer to a function in the generated code.
|
|
65
65
|
*/
|
|
66
66
|
export type FunctionName = z.infer<typeof zFunctionName>;
|
|
67
|
-
export declare const toFunctionName: (input: string) => string & z.
|
|
68
|
-
declare const zVariableName: z.ZodBranded<z.ZodBranded<z.ZodString, "PXL.VariableName">, "PXL.Importable">;
|
|
67
|
+
export declare const toFunctionName: (input: string) => string & z.core.$brand<"PXL.FunctionName"> & z.core.$brand<"PXL.Importable">;
|
|
68
|
+
declare const zVariableName: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "PXL.VariableName", "out">, "PXL.Importable", "out">;
|
|
69
69
|
/**
|
|
70
70
|
* A variable name that is used to refer to a variable in the generated code.
|
|
71
71
|
*/
|
|
72
72
|
export type VariableName = z.infer<typeof zVariableName>;
|
|
73
|
-
export declare const toVariableName: (input: string) => string & z.
|
|
74
|
-
declare const zPostXlPackageName: z.ZodBranded<z.
|
|
73
|
+
export declare const toVariableName: (input: string) => string & z.core.$brand<"PXL.VariableName"> & z.core.$brand<"PXL.Importable">;
|
|
74
|
+
declare const zPostXlPackageName: z.core.$ZodBranded<z.ZodString, "PXL.PackageName", "out">;
|
|
75
75
|
/**
|
|
76
76
|
* A package name that is used to refer to a package in the generated code.
|
|
77
77
|
*/
|
|
78
|
-
export declare const toPostXlPackageName: (input: string) => string & z.
|
|
79
|
-
export declare const toPackageName: (input: string) => string & z.
|
|
78
|
+
export declare const toPostXlPackageName: (input: string) => string & z.core.$brand<"PXL.PackageName">;
|
|
79
|
+
export declare const toPackageName: (input: string) => string & z.core.$brand<"PXL.PackageName">;
|
|
80
80
|
/**
|
|
81
81
|
* A package name that is used to refer to a package name that is provided via
|
|
82
82
|
* in package.json or is a NodeJS module.
|
|
83
83
|
* E.g. "fs", "random", "@nestjs/common".
|
|
84
84
|
*/
|
|
85
85
|
export type PackageName = z.infer<typeof zPostXlPackageName>;
|
|
86
|
-
declare const zClassName: z.ZodBranded<z.ZodBranded<z.ZodString, "PXL.ClassName">, "PXL.Importable">;
|
|
86
|
+
declare const zClassName: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "PXL.ClassName", "out">, "PXL.Importable", "out">;
|
|
87
87
|
/**
|
|
88
88
|
* A class name that is used to refer to a class in the generated code.
|
|
89
89
|
*/
|
|
90
90
|
export type ClassName = z.infer<typeof zClassName>;
|
|
91
|
-
export declare const toClassName: (input: string) => string & z.
|
|
92
|
-
declare const zConstantName: z.ZodBranded<z.ZodBranded<z.ZodString, "PXL.ConstantName">, "PXL.Importable">;
|
|
91
|
+
export declare const toClassName: (input: string) => string & z.core.$brand<"PXL.ClassName"> & z.core.$brand<"PXL.Importable">;
|
|
92
|
+
declare const zConstantName: z.core.$ZodBranded<z.core.$ZodBranded<z.ZodString, "PXL.ConstantName", "out">, "PXL.Importable", "out">;
|
|
93
93
|
/**
|
|
94
94
|
* A constant name that is used to refer to a constant in the generated code.
|
|
95
95
|
*/
|
|
96
96
|
export type ConstantName = z.infer<typeof zConstantName>;
|
|
97
|
-
export declare const toConstantName: (input: string) => string & z.
|
|
98
|
-
declare const zConstantValue: z.ZodBranded<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, "PXL.ConstantValue">;
|
|
97
|
+
export declare const toConstantName: (input: string) => string & z.core.$brand<"PXL.ConstantName"> & z.core.$brand<"PXL.Importable">;
|
|
98
|
+
declare const zConstantValue: z.core.$ZodBranded<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, "PXL.ConstantValue", "out">;
|
|
99
99
|
/**
|
|
100
100
|
* A constant value that is used in the generated code.
|
|
101
101
|
*/
|
|
102
102
|
export type ConstantValue = z.infer<typeof zConstantValue>;
|
|
103
|
-
export declare const toConstantValue: (input: string | number | boolean | null) => (string | number
|
|
104
|
-
declare const zDiscriminantValue: z.ZodBranded<z.ZodString, "PXL.DiscriminantValue">;
|
|
103
|
+
export declare const toConstantValue: (input: string | number | boolean | null) => (string & z.core.$brand<"PXL.ConstantValue">) | (number & z.core.$brand<"PXL.ConstantValue">) | (false & z.core.$brand<"PXL.ConstantValue">) | (true & z.core.$brand<"PXL.ConstantValue">);
|
|
104
|
+
declare const zDiscriminantValue: z.core.$ZodBranded<z.ZodString, "PXL.DiscriminantValue", "out">;
|
|
105
105
|
/**
|
|
106
106
|
* A discriminant value, used to discriminate union types.
|
|
107
107
|
*/
|
|
108
108
|
export type DiscriminantValue = z.infer<typeof zDiscriminantValue>;
|
|
109
|
-
export declare const toDiscriminantValue: (input: string) => string & z.
|
|
110
|
-
declare const zFileName: z.ZodBranded<z.ZodString, "PXL.FileName">;
|
|
109
|
+
export declare const toDiscriminantValue: (input: string) => string & z.core.$brand<"PXL.DiscriminantValue">;
|
|
110
|
+
declare const zFileName: z.core.$ZodBranded<z.ZodString, "PXL.FileName", "out">;
|
|
111
111
|
/**
|
|
112
112
|
* A file name that is used to refer to a file in the generated code.
|
|
113
113
|
*/
|
|
114
114
|
export type FileName = z.infer<typeof zFileName>;
|
|
115
|
-
export declare const toFileName: (input: string) => string & z.
|
|
116
|
-
declare const zFolderName: z.ZodBranded<z.ZodString, "PXL.FolderName">;
|
|
115
|
+
export declare const toFileName: (input: string) => string & z.core.$brand<"PXL.FileName">;
|
|
116
|
+
declare const zFolderName: z.core.$ZodBranded<z.ZodString, "PXL.FolderName", "out">;
|
|
117
117
|
/**
|
|
118
118
|
* A folder name that is used to refer to a folder in the generated code.
|
|
119
119
|
*/
|
|
120
120
|
export type FolderName = z.infer<typeof zFolderName>;
|
|
121
|
-
export declare const toFolderName: (input: string) => string & z.
|
|
122
|
-
declare const zFilePath: z.ZodBranded<z.ZodString, "PXL.FilePath">;
|
|
121
|
+
export declare const toFolderName: (input: string) => string & z.core.$brand<"PXL.FolderName">;
|
|
122
|
+
declare const zFilePath: z.core.$ZodBranded<z.ZodString, "PXL.FilePath", "out">;
|
|
123
123
|
/**
|
|
124
124
|
* A file path that is used to refer to a file in the generated code.
|
|
125
125
|
*/
|
|
126
126
|
export type FilePath = z.infer<typeof zFilePath>;
|
|
127
|
-
export declare const toFilePath: (input: string) => string & z.
|
|
128
|
-
declare const zBackendModuleName: z.ZodBranded<z.ZodString, "PXL.BackendModuleName">;
|
|
127
|
+
export declare const toFilePath: (input: string) => string & z.core.$brand<"PXL.FilePath">;
|
|
128
|
+
declare const zBackendModuleName: z.core.$ZodBranded<z.ZodString, "PXL.BackendModuleName", "out">;
|
|
129
129
|
/**
|
|
130
130
|
* A backend module name that is used to refer to a module in the backend.
|
|
131
131
|
* E.g. `@actions`.
|
|
132
132
|
*/
|
|
133
133
|
export type BackendModuleName = z.infer<typeof zBackendModuleName>;
|
|
134
|
-
export declare const toBackendModuleName: (input: string) => string & z.
|
|
135
|
-
declare const zBackendModuleLocation: z.ZodBranded<z.ZodString, "PXL.BackendModuleLocation">;
|
|
134
|
+
export declare const toBackendModuleName: (input: string) => string & z.core.$brand<"PXL.BackendModuleName">;
|
|
135
|
+
declare const zBackendModuleLocation: z.core.$ZodBranded<z.ZodString, "PXL.BackendModuleLocation", "out">;
|
|
136
136
|
/**
|
|
137
137
|
* A module location is a reference to a file in a locale backend module.
|
|
138
138
|
* E.g. `@actions/actions.types`.
|
|
@@ -143,7 +143,7 @@ declare const zBackendModuleLocation: z.ZodBranded<z.ZodString, "PXL.BackendModu
|
|
|
143
143
|
* Therefore, we need to reference the actual file together with the package name.
|
|
144
144
|
*/
|
|
145
145
|
export type BackendModuleLocation = z.infer<typeof zBackendModuleLocation>;
|
|
146
|
-
export declare const toBackendModuleLocation: (input: `@${string}`) => string & z.
|
|
146
|
+
export declare const toBackendModuleLocation: (input: `@${string}`) => string & z.core.$brand<"PXL.BackendModuleLocation">;
|
|
147
147
|
export type ImportableTypes = TypeName | FunctionName | ClassName | ConstantName | AnnotatedTypeName;
|
|
148
148
|
export type ImportPaths = FilePath | PackageName | BackendModuleLocation;
|
|
149
149
|
export {};
|
|
@@ -64,7 +64,9 @@ const toVariableName = (input) => zVariableName.parse(input);
|
|
|
64
64
|
exports.toVariableName = toVariableName;
|
|
65
65
|
const zPostXlPackageName = zod_1.default
|
|
66
66
|
.string()
|
|
67
|
-
.refine((name) => name.startsWith('@postxl/'),
|
|
67
|
+
.refine((name) => name.startsWith('@postxl/'), {
|
|
68
|
+
error: (issue) => `Package name must start with "@postxl/", got "${issue.input}"!`,
|
|
69
|
+
})
|
|
68
70
|
.brand('PXL.PackageName');
|
|
69
71
|
/**
|
|
70
72
|
* A package name that is used to refer to a package in the generated code.
|
package/dist/utils/checksum.d.ts
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Block Preservation for Merge Conflicts
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to preserve custom code blocks during
|
|
5
|
+
* merge conflict generation. Developers can mark sections of code with special
|
|
6
|
+
* comment markers, and these sections will be automatically preserved when
|
|
7
|
+
* the generator runs.
|
|
8
|
+
*
|
|
9
|
+
* ## Usage
|
|
10
|
+
*
|
|
11
|
+
* Mark custom code blocks in your ejected files:
|
|
12
|
+
*
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // @custom-start
|
|
15
|
+
* // your custom code here
|
|
16
|
+
* // @custom-end
|
|
17
|
+
*
|
|
18
|
+
* // Or with a name for clarity:
|
|
19
|
+
* // @custom-start:myFeature
|
|
20
|
+
* // your custom code here
|
|
21
|
+
* // @custom-end:myFeature
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* When the generator runs and would normally create merge conflicts,
|
|
25
|
+
* it will:
|
|
26
|
+
* 1. Extract custom blocks from the modified file
|
|
27
|
+
* 2. Find appropriate anchor points in the generated content
|
|
28
|
+
* 3. Re-insert custom blocks at the correct positions
|
|
29
|
+
* 4. Only create merge conflict markers for actual conflicts
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Marker patterns for custom blocks
|
|
33
|
+
*
|
|
34
|
+
* Supported formats:
|
|
35
|
+
* - Line comments: // @custom-start or // @custom-start:name
|
|
36
|
+
* - Block comments: Must be on a single line with both delimiters
|
|
37
|
+
*
|
|
38
|
+
* Note: Multi-line block comments are NOT supported. The opening and closing
|
|
39
|
+
* delimiters must be on the same line as the marker. This simplifies parsing
|
|
40
|
+
* and avoids ambiguity. See tests for examples.
|
|
41
|
+
*/
|
|
42
|
+
export declare const customBlockMarkers: {
|
|
43
|
+
readonly startPattern: RegExp;
|
|
44
|
+
readonly endPattern: RegExp;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Represents a custom block extracted from source code
|
|
48
|
+
*/
|
|
49
|
+
export type CustomBlock = {
|
|
50
|
+
/** Optional name of the block (from @custom-start:name) */
|
|
51
|
+
name: string | undefined;
|
|
52
|
+
/** The lines of content within the block (including markers) */
|
|
53
|
+
lines: string[];
|
|
54
|
+
/** The line index where the block starts (0-based) */
|
|
55
|
+
startLineIndex: number;
|
|
56
|
+
/** The line index where the block ends (0-based, inclusive) */
|
|
57
|
+
endLineIndex: number;
|
|
58
|
+
/** Anchor context: non-empty lines before the block for positioning */
|
|
59
|
+
anchorBefore: string[];
|
|
60
|
+
/** Anchor context: non-empty lines after the block for positioning */
|
|
61
|
+
anchorAfter: string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Result of extracting custom blocks from source code
|
|
65
|
+
*/
|
|
66
|
+
export type ExtractResult = {
|
|
67
|
+
/** Successfully extracted custom blocks */
|
|
68
|
+
blocks: CustomBlock[];
|
|
69
|
+
/** Lines that are not part of any custom block */
|
|
70
|
+
nonCustomLines: string[];
|
|
71
|
+
/** Indices of non-custom lines in the original source */
|
|
72
|
+
nonCustomLineIndices: number[];
|
|
73
|
+
/** Any errors encountered during extraction */
|
|
74
|
+
errors: CustomBlockError[];
|
|
75
|
+
};
|
|
76
|
+
export type CustomBlockError = {
|
|
77
|
+
type: 'unclosed_block' | 'unexpected_end' | 'mismatched_name';
|
|
78
|
+
message: string;
|
|
79
|
+
lineIndex: number;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Extracts custom blocks from source code content.
|
|
83
|
+
*
|
|
84
|
+
* @param content - The source code content as a string
|
|
85
|
+
* @returns ExtractResult containing blocks, non-custom lines, and any errors
|
|
86
|
+
*/
|
|
87
|
+
export declare function extractCustomBlocks(content: string): ExtractResult;
|
|
88
|
+
/**
|
|
89
|
+
* Finds the best position to insert a custom block in the target content
|
|
90
|
+
* based on anchor context matching.
|
|
91
|
+
*
|
|
92
|
+
* @param block - The custom block to insert
|
|
93
|
+
* @param targetLines - The target content lines
|
|
94
|
+
* @returns The line index where the block should be inserted, or null if no good position found
|
|
95
|
+
*/
|
|
96
|
+
export declare function findInsertionPosition(block: CustomBlock, targetLines: string[]): number | null;
|
|
97
|
+
/**
|
|
98
|
+
* Inserts custom blocks into the target content at their appropriate positions.
|
|
99
|
+
*
|
|
100
|
+
* @param blocks - The custom blocks to insert
|
|
101
|
+
* @param targetContent - The target content to insert blocks into
|
|
102
|
+
* @returns Object containing the modified content and any blocks that couldn't be placed
|
|
103
|
+
*/
|
|
104
|
+
export declare function insertCustomBlocks(blocks: CustomBlock[], targetContent: string): {
|
|
105
|
+
content: string;
|
|
106
|
+
unplacedBlocks: CustomBlock[];
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Checks if a string contains any custom block markers
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasCustomBlockMarkers(content: string): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Reconstructs content from non-custom lines, used when comparing
|
|
114
|
+
* the "real" content differences (excluding custom blocks)
|
|
115
|
+
*/
|
|
116
|
+
export declare function reconstructNonCustomContent(extractResult: ExtractResult): string;
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Custom Block Preservation for Merge Conflicts
|
|
4
|
+
*
|
|
5
|
+
* This module provides functionality to preserve custom code blocks during
|
|
6
|
+
* merge conflict generation. Developers can mark sections of code with special
|
|
7
|
+
* comment markers, and these sections will be automatically preserved when
|
|
8
|
+
* the generator runs.
|
|
9
|
+
*
|
|
10
|
+
* ## Usage
|
|
11
|
+
*
|
|
12
|
+
* Mark custom code blocks in your ejected files:
|
|
13
|
+
*
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // @custom-start
|
|
16
|
+
* // your custom code here
|
|
17
|
+
* // @custom-end
|
|
18
|
+
*
|
|
19
|
+
* // Or with a name for clarity:
|
|
20
|
+
* // @custom-start:myFeature
|
|
21
|
+
* // your custom code here
|
|
22
|
+
* // @custom-end:myFeature
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* When the generator runs and would normally create merge conflicts,
|
|
26
|
+
* it will:
|
|
27
|
+
* 1. Extract custom blocks from the modified file
|
|
28
|
+
* 2. Find appropriate anchor points in the generated content
|
|
29
|
+
* 3. Re-insert custom blocks at the correct positions
|
|
30
|
+
* 4. Only create merge conflict markers for actual conflicts
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.customBlockMarkers = void 0;
|
|
34
|
+
exports.extractCustomBlocks = extractCustomBlocks;
|
|
35
|
+
exports.findInsertionPosition = findInsertionPosition;
|
|
36
|
+
exports.insertCustomBlocks = insertCustomBlocks;
|
|
37
|
+
exports.hasCustomBlockMarkers = hasCustomBlockMarkers;
|
|
38
|
+
exports.reconstructNonCustomContent = reconstructNonCustomContent;
|
|
39
|
+
/**
|
|
40
|
+
* Marker patterns for custom blocks
|
|
41
|
+
*
|
|
42
|
+
* Supported formats:
|
|
43
|
+
* - Line comments: // @custom-start or // @custom-start:name
|
|
44
|
+
* - Block comments: Must be on a single line with both delimiters
|
|
45
|
+
*
|
|
46
|
+
* Note: Multi-line block comments are NOT supported. The opening and closing
|
|
47
|
+
* delimiters must be on the same line as the marker. This simplifies parsing
|
|
48
|
+
* and avoids ambiguity. See tests for examples.
|
|
49
|
+
*/
|
|
50
|
+
exports.customBlockMarkers = {
|
|
51
|
+
// Matches: // @custom-start or // @custom-start:name
|
|
52
|
+
// Also supports: /* @custom-start */ or /* @custom-start:name */
|
|
53
|
+
startPattern: /^\s*(?:\/\/|\/\*)\s*@custom-start(?::([a-zA-Z0-9_-]+))?\s*(?:\*\/)?\s*$/,
|
|
54
|
+
// Matches: // @custom-end or // @custom-end:name
|
|
55
|
+
// Also supports: /* @custom-end */ or /* @custom-end:name */
|
|
56
|
+
endPattern: /^\s*(?:\/\/|\/\*)\s*@custom-end(?::([a-zA-Z0-9_-]+))?\s*(?:\*\/)?\s*$/,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Number of non-empty lines to capture before/after a custom block for anchoring
|
|
60
|
+
*/
|
|
61
|
+
const ANCHOR_CONTEXT_LINES = 3;
|
|
62
|
+
/**
|
|
63
|
+
* Extracts the block name from a regex match result
|
|
64
|
+
*/
|
|
65
|
+
function extractBlockName(match) {
|
|
66
|
+
return match[1] ?? undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Processes a @custom-start marker
|
|
70
|
+
*/
|
|
71
|
+
function processStartMarker(line, lineIndex, startMatch, currentBlock, errors) {
|
|
72
|
+
if (!currentBlock) {
|
|
73
|
+
// Starting a new custom block
|
|
74
|
+
return {
|
|
75
|
+
name: extractBlockName(startMatch),
|
|
76
|
+
lines: [line],
|
|
77
|
+
startLineIndex: lineIndex,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Nested @custom-start - this is an error
|
|
81
|
+
errors.push({
|
|
82
|
+
type: 'unclosed_block',
|
|
83
|
+
message: `Found @custom-start while already inside a custom block${currentBlock.name ? ` (${currentBlock.name})` : ''}. Nested custom blocks are not supported.`,
|
|
84
|
+
lineIndex,
|
|
85
|
+
});
|
|
86
|
+
currentBlock.lines.push(line);
|
|
87
|
+
return currentBlock;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Processes a @custom-end marker
|
|
91
|
+
* Always returns null since processing an end marker closes the current block
|
|
92
|
+
*/
|
|
93
|
+
function processEndMarker(line, lineIndex, endMatch, currentBlock, lines, blocks, errors) {
|
|
94
|
+
if (!currentBlock) {
|
|
95
|
+
// @custom-end without a matching start
|
|
96
|
+
errors.push({
|
|
97
|
+
type: 'unexpected_end',
|
|
98
|
+
message: `Found @custom-end without a matching @custom-start`,
|
|
99
|
+
lineIndex,
|
|
100
|
+
});
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const endName = extractBlockName(endMatch);
|
|
104
|
+
// Check for name mismatch
|
|
105
|
+
if (currentBlock.name !== endName) {
|
|
106
|
+
errors.push({
|
|
107
|
+
type: 'mismatched_name',
|
|
108
|
+
message: `Custom block name mismatch: started with "${currentBlock.name ?? '(unnamed)'}" but ended with "${endName ?? '(unnamed)'}"`,
|
|
109
|
+
lineIndex,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
currentBlock.lines.push(line);
|
|
113
|
+
// Capture anchor context
|
|
114
|
+
const anchorBefore = captureAnchorContext(lines, currentBlock.startLineIndex, 'before');
|
|
115
|
+
const anchorAfter = captureAnchorContext(lines, lineIndex, 'after');
|
|
116
|
+
blocks.push({
|
|
117
|
+
name: currentBlock.name,
|
|
118
|
+
lines: currentBlock.lines,
|
|
119
|
+
startLineIndex: currentBlock.startLineIndex,
|
|
120
|
+
endLineIndex: lineIndex,
|
|
121
|
+
anchorBefore,
|
|
122
|
+
anchorAfter,
|
|
123
|
+
});
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Adds unclosed block's lines to non-custom lines
|
|
128
|
+
*/
|
|
129
|
+
function processUnclosedBlock(currentBlock, nonCustomLines, nonCustomLineIndices, errors) {
|
|
130
|
+
errors.push({
|
|
131
|
+
type: 'unclosed_block',
|
|
132
|
+
message: `Custom block${currentBlock.name ? ` (${currentBlock.name})` : ''} was never closed`,
|
|
133
|
+
lineIndex: currentBlock.startLineIndex,
|
|
134
|
+
});
|
|
135
|
+
// Add the unclosed block's lines to non-custom lines
|
|
136
|
+
for (let i = 0; i < currentBlock.lines.length; i++) {
|
|
137
|
+
const blockLine = currentBlock.lines[i];
|
|
138
|
+
if (blockLine === undefined) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
nonCustomLines.push(blockLine);
|
|
142
|
+
nonCustomLineIndices.push(currentBlock.startLineIndex + i);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Extracts custom blocks from source code content.
|
|
147
|
+
*
|
|
148
|
+
* @param content - The source code content as a string
|
|
149
|
+
* @returns ExtractResult containing blocks, non-custom lines, and any errors
|
|
150
|
+
*/
|
|
151
|
+
function extractCustomBlocks(content) {
|
|
152
|
+
const lines = content.split('\n');
|
|
153
|
+
const blocks = [];
|
|
154
|
+
const errors = [];
|
|
155
|
+
const nonCustomLines = [];
|
|
156
|
+
const nonCustomLineIndices = [];
|
|
157
|
+
let currentBlock = null;
|
|
158
|
+
for (let i = 0; i < lines.length; i++) {
|
|
159
|
+
const line = lines[i];
|
|
160
|
+
if (line === undefined) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const startMatch = exports.customBlockMarkers.startPattern.exec(line);
|
|
164
|
+
const endMatch = exports.customBlockMarkers.endPattern.exec(line);
|
|
165
|
+
if (startMatch) {
|
|
166
|
+
currentBlock = processStartMarker(line, i, startMatch, currentBlock, errors);
|
|
167
|
+
}
|
|
168
|
+
else if (endMatch) {
|
|
169
|
+
const wasInBlock = currentBlock !== null;
|
|
170
|
+
currentBlock = processEndMarker(line, i, endMatch, currentBlock, lines, blocks, errors);
|
|
171
|
+
// If we weren't in a block, this line goes to non-custom lines
|
|
172
|
+
if (!wasInBlock) {
|
|
173
|
+
nonCustomLines.push(line);
|
|
174
|
+
nonCustomLineIndices.push(i);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (currentBlock) {
|
|
178
|
+
// Inside a custom block, add to current block's lines
|
|
179
|
+
currentBlock.lines.push(line);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Not inside a custom block
|
|
183
|
+
nonCustomLines.push(line);
|
|
184
|
+
nonCustomLineIndices.push(i);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Check for unclosed block at end of file
|
|
188
|
+
if (currentBlock) {
|
|
189
|
+
processUnclosedBlock(currentBlock, nonCustomLines, nonCustomLineIndices, errors);
|
|
190
|
+
}
|
|
191
|
+
return { blocks, nonCustomLines, nonCustomLineIndices, errors };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Captures anchor context lines (non-empty, significant lines) before or after a position
|
|
195
|
+
*/
|
|
196
|
+
function captureAnchorContext(lines, position, direction) {
|
|
197
|
+
const anchors = [];
|
|
198
|
+
const step = direction === 'before' ? -1 : 1;
|
|
199
|
+
const start = direction === 'before' ? position - 1 : position + 1;
|
|
200
|
+
for (let i = start; direction === 'before' ? i >= 0 : i < lines.length; i += step) {
|
|
201
|
+
const line = lines[i];
|
|
202
|
+
if (line === undefined) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Skip empty lines and custom block markers
|
|
206
|
+
if (isSignificantLine(line)) {
|
|
207
|
+
anchors.push(normalizeAnchorLine(line));
|
|
208
|
+
if (anchors.length >= ANCHOR_CONTEXT_LINES) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Reverse "before" anchors so they're in original order (closest to block first)
|
|
214
|
+
if (direction === 'before') {
|
|
215
|
+
anchors.reverse();
|
|
216
|
+
}
|
|
217
|
+
return anchors;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Checks if a line is significant for anchoring purposes
|
|
221
|
+
* (not empty, not a comment-only line, not a custom block marker)
|
|
222
|
+
*/
|
|
223
|
+
function isSignificantLine(line) {
|
|
224
|
+
const trimmed = line.trim();
|
|
225
|
+
if (trimmed === '') {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (exports.customBlockMarkers.startPattern.test(line)) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
if (exports.customBlockMarkers.endPattern.test(line)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
// Consider all other lines significant (including comments that might be documentation)
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Normalizes a line for anchor comparison (trims whitespace)
|
|
239
|
+
*/
|
|
240
|
+
function normalizeAnchorLine(line) {
|
|
241
|
+
return line.trim();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Finds the best position to insert a custom block in the target content
|
|
245
|
+
* based on anchor context matching.
|
|
246
|
+
*
|
|
247
|
+
* @param block - The custom block to insert
|
|
248
|
+
* @param targetLines - The target content lines
|
|
249
|
+
* @returns The line index where the block should be inserted, or null if no good position found
|
|
250
|
+
*/
|
|
251
|
+
function findInsertionPosition(block, targetLines) {
|
|
252
|
+
// Strategy 1: Try to find a match using "before" anchors
|
|
253
|
+
// We look for the anchor sequence and insert after the last matched anchor
|
|
254
|
+
const beforePosition = findAnchorSequence(block.anchorBefore, targetLines, 'before');
|
|
255
|
+
if (beforePosition !== null) {
|
|
256
|
+
return beforePosition + 1; // Insert after the anchor
|
|
257
|
+
}
|
|
258
|
+
// Strategy 2: Try to find a match using "after" anchors
|
|
259
|
+
// We look for the anchor sequence and insert before the first matched anchor
|
|
260
|
+
const afterPosition = findAnchorSequence(block.anchorAfter, targetLines, 'after');
|
|
261
|
+
if (afterPosition !== null) {
|
|
262
|
+
return afterPosition; // Insert before the anchor
|
|
263
|
+
}
|
|
264
|
+
// Strategy 3: Try to find any single anchor match
|
|
265
|
+
const singleAnchor = findSingleAnchorMatch(block, targetLines);
|
|
266
|
+
if (singleAnchor !== null) {
|
|
267
|
+
return singleAnchor;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Finds a sequence of anchor lines in the target content
|
|
273
|
+
*/
|
|
274
|
+
function findAnchorSequence(anchors, targetLines, mode) {
|
|
275
|
+
if (anchors.length === 0) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
// For "before" mode, we want to find the sequence and return the position of the last anchor
|
|
279
|
+
// For "after" mode, we want to find the sequence and return the position of the first anchor
|
|
280
|
+
// Try to match progressively fewer anchors (from all to just one)
|
|
281
|
+
for (let matchCount = anchors.length; matchCount >= 1; matchCount--) {
|
|
282
|
+
const anchorsToMatch = mode === 'before'
|
|
283
|
+
? anchors.slice(-matchCount) // Take last N anchors for "before"
|
|
284
|
+
: anchors.slice(0, matchCount); // Take first N anchors for "after"
|
|
285
|
+
const position = findSequenceInTarget(anchorsToMatch, targetLines, mode);
|
|
286
|
+
if (position !== null) {
|
|
287
|
+
return position;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Finds the next significant line index starting from a given position
|
|
294
|
+
*/
|
|
295
|
+
function findNextSignificantLineIndex(targetLines, startIndex) {
|
|
296
|
+
for (let i = startIndex; i < targetLines.length; i++) {
|
|
297
|
+
const line = targetLines[i];
|
|
298
|
+
if (line !== undefined && isSignificantLine(line)) {
|
|
299
|
+
return i;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Checks if a sequence matches at a given position in target lines
|
|
306
|
+
*/
|
|
307
|
+
function matchSequenceAtPosition(sequence, targetLines, startIndex) {
|
|
308
|
+
let lastMatchIndex = startIndex;
|
|
309
|
+
for (let j = 1; j < sequence.length; j++) {
|
|
310
|
+
const expectedAnchor = sequence[j];
|
|
311
|
+
if (expectedAnchor === undefined) {
|
|
312
|
+
return { matches: false, lastMatchIndex };
|
|
313
|
+
}
|
|
314
|
+
// Find the next significant line in target
|
|
315
|
+
const nextIndex = findNextSignificantLineIndex(targetLines, lastMatchIndex + 1);
|
|
316
|
+
if (nextIndex === null) {
|
|
317
|
+
return { matches: false, lastMatchIndex };
|
|
318
|
+
}
|
|
319
|
+
const nextTargetLine = targetLines[nextIndex];
|
|
320
|
+
if (nextTargetLine === undefined || normalizeAnchorLine(nextTargetLine) !== expectedAnchor) {
|
|
321
|
+
return { matches: false, lastMatchIndex };
|
|
322
|
+
}
|
|
323
|
+
lastMatchIndex = nextIndex;
|
|
324
|
+
}
|
|
325
|
+
return { matches: true, lastMatchIndex };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Finds a specific sequence of lines in the target
|
|
329
|
+
*/
|
|
330
|
+
function findSequenceInTarget(sequence, targetLines, mode) {
|
|
331
|
+
if (sequence.length === 0) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const firstAnchor = sequence[0];
|
|
335
|
+
if (firstAnchor === undefined) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
for (let i = 0; i < targetLines.length; i++) {
|
|
339
|
+
const targetLine = targetLines[i];
|
|
340
|
+
if (targetLine === undefined) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (normalizeAnchorLine(targetLine) === firstAnchor) {
|
|
344
|
+
const { matches, lastMatchIndex } = matchSequenceAtPosition(sequence, targetLines, i);
|
|
345
|
+
if (matches) {
|
|
346
|
+
return mode === 'before' ? lastMatchIndex : i;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Finds a single anchor in target lines
|
|
354
|
+
*/
|
|
355
|
+
function findAnchorInTarget(anchor, targetLines) {
|
|
356
|
+
for (let i = 0; i < targetLines.length; i++) {
|
|
357
|
+
const targetLine = targetLines[i];
|
|
358
|
+
if (targetLine === undefined) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (normalizeAnchorLine(targetLine) === anchor) {
|
|
362
|
+
return i;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Finds a single anchor match as a fallback
|
|
369
|
+
*/
|
|
370
|
+
function findSingleAnchorMatch(block, targetLines) {
|
|
371
|
+
// Try "before" anchors first (prefer closest anchor)
|
|
372
|
+
for (let i = block.anchorBefore.length - 1; i >= 0; i--) {
|
|
373
|
+
const anchor = block.anchorBefore[i];
|
|
374
|
+
if (anchor === undefined) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const position = findAnchorInTarget(anchor, targetLines);
|
|
378
|
+
if (position !== null) {
|
|
379
|
+
return position + 1; // Insert after this anchor
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Try "after" anchors
|
|
383
|
+
for (const anchor of block.anchorAfter) {
|
|
384
|
+
const position = findAnchorInTarget(anchor, targetLines);
|
|
385
|
+
if (position !== null) {
|
|
386
|
+
return position; // Insert before this anchor
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Inserts custom blocks into the target content at their appropriate positions.
|
|
393
|
+
*
|
|
394
|
+
* @param blocks - The custom blocks to insert
|
|
395
|
+
* @param targetContent - The target content to insert blocks into
|
|
396
|
+
* @returns Object containing the modified content and any blocks that couldn't be placed
|
|
397
|
+
*/
|
|
398
|
+
function insertCustomBlocks(blocks, targetContent) {
|
|
399
|
+
if (blocks.length === 0) {
|
|
400
|
+
return { content: targetContent, unplacedBlocks: [] };
|
|
401
|
+
}
|
|
402
|
+
const targetLines = targetContent.split('\n');
|
|
403
|
+
const unplacedBlocks = [];
|
|
404
|
+
// Sort blocks by their insertion position (reverse order to maintain indices)
|
|
405
|
+
const blocksWithPositions = blocks.map((block) => ({
|
|
406
|
+
block,
|
|
407
|
+
position: findInsertionPosition(block, targetLines),
|
|
408
|
+
}));
|
|
409
|
+
// Separate placed and unplaced blocks
|
|
410
|
+
const placedBlocks = blocksWithPositions
|
|
411
|
+
.filter((b) => b.position !== null)
|
|
412
|
+
.sort((a, b) => b.position - a.position); // Sort descending to insert from end
|
|
413
|
+
for (const { block, position } of blocksWithPositions) {
|
|
414
|
+
if (position === null) {
|
|
415
|
+
unplacedBlocks.push(block);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Insert blocks from end to beginning to maintain correct positions
|
|
419
|
+
for (const { block, position } of placedBlocks) {
|
|
420
|
+
// Add an empty line before the block if the previous line is not empty
|
|
421
|
+
const prevLine = position > 0 ? targetLines[position - 1] : undefined;
|
|
422
|
+
const needsEmptyLineBefore = prevLine !== undefined && prevLine.trim() !== '';
|
|
423
|
+
// Add an empty line after the block if the next line is not empty
|
|
424
|
+
const nextLine = position < targetLines.length ? targetLines[position] : undefined;
|
|
425
|
+
const needsEmptyLineAfter = nextLine !== undefined && nextLine.trim() !== '';
|
|
426
|
+
const insertLines = [];
|
|
427
|
+
if (needsEmptyLineBefore) {
|
|
428
|
+
insertLines.push('');
|
|
429
|
+
}
|
|
430
|
+
insertLines.push(...block.lines);
|
|
431
|
+
if (needsEmptyLineAfter) {
|
|
432
|
+
insertLines.push('');
|
|
433
|
+
}
|
|
434
|
+
targetLines.splice(position, 0, ...insertLines);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
content: targetLines.join('\n'),
|
|
438
|
+
unplacedBlocks,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Checks if a string contains any custom block markers
|
|
443
|
+
*/
|
|
444
|
+
function hasCustomBlockMarkers(content) {
|
|
445
|
+
const lines = content.split('\n');
|
|
446
|
+
return lines.some((line) => exports.customBlockMarkers.startPattern.test(line) || exports.customBlockMarkers.endPattern.test(line));
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Reconstructs content from non-custom lines, used when comparing
|
|
450
|
+
* the "real" content differences (excluding custom blocks)
|
|
451
|
+
*/
|
|
452
|
+
function reconstructNonCustomContent(extractResult) {
|
|
453
|
+
return extractResult.nonCustomLines.join('\n');
|
|
454
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./custom-blocks"), exports);
|
|
17
18
|
__exportStar(require("./jsdoc"), exports);
|
|
18
19
|
__exportStar(require("./lint"), exports);
|
|
19
20
|
__exportStar(require("./path"), exports);
|
package/dist/utils/lockfile.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { VirtualFileSystem } from './vfs.class';
|
|
3
|
-
declare const zLockFile: z.
|
|
3
|
+
declare const zLockFile: z.ZodPipe<z.ZodRecord<z.core.$ZodBranded<z.ZodString, "PXL.PosixPath", "out">, z.core.$ZodBranded<z.ZodString, "PXL.Checksum", "out">>, z.ZodTransform<Map<string & z.core.$brand<"PXL.PosixPath">, string & z.core.$brand<"PXL.Checksum">>, Record<string & z.core.$brand<"PXL.PosixPath">, string & z.core.$brand<"PXL.Checksum">>>>;
|
|
4
4
|
type LockFile = z.infer<typeof zLockFile>;
|
|
5
5
|
export declare function writeLockFile(lockFilePath: string, vfs: VirtualFileSystem): Promise<void>;
|
|
6
6
|
export declare function readLockFile(lockFilePath: string): Promise<LockFile | undefined>;
|
|
@@ -17,11 +17,23 @@ export declare function hasMergeConflictMarkers(content: FileContent): boolean;
|
|
|
17
17
|
/**
|
|
18
18
|
* Generates a merged output with conflict markers between two file contents.
|
|
19
19
|
*
|
|
20
|
+
* This function supports custom block preservation: if the source content contains
|
|
21
|
+
* custom blocks marked with `// @custom-start` and `// @custom-end` comments,
|
|
22
|
+
* those blocks will be automatically inserted into the generated content at their
|
|
23
|
+
* appropriate positions (based on anchor context), avoiding unnecessary merge conflicts
|
|
24
|
+
* for custom code that was intentionally added.
|
|
25
|
+
*
|
|
20
26
|
* @param contentSource - The content of the first file as a string.
|
|
21
27
|
* @param contentIncoming - The content of the second file as a string.
|
|
22
28
|
* @param labelSource - Optional label for the first file in conflict markers.
|
|
23
29
|
* @param labelIncoming - Optional label for the second file in conflict markers.
|
|
24
30
|
* @returns The merged content with conflict markers as a string.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Mark custom code in ejected files:
|
|
34
|
+
* // @custom-start:myFeature
|
|
35
|
+
* // your custom code here
|
|
36
|
+
* // @custom-end:myFeature
|
|
25
37
|
*/
|
|
26
38
|
export declare function generateMergeConflict({ contentSource, contentIncoming, labelSource, labelIncoming, }: {
|
|
27
39
|
contentSource: FileContent;
|
|
@@ -39,6 +39,7 @@ exports.generateMergeConflict = generateMergeConflict;
|
|
|
39
39
|
exports.generateDiffSummary = generateDiffSummary;
|
|
40
40
|
const Diff = __importStar(require("diff"));
|
|
41
41
|
const utils_1 = require("@postxl/utils");
|
|
42
|
+
const custom_blocks_1 = require("./custom-blocks");
|
|
42
43
|
/**
|
|
43
44
|
* Standard merge conflict markers used by git and our generator
|
|
44
45
|
*/
|
|
@@ -84,6 +85,28 @@ function normalizeWhitespace(content) {
|
|
|
84
85
|
}
|
|
85
86
|
return lines.join('\n');
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalizes whitespace more aggressively for semantic comparison.
|
|
90
|
+
* Used when comparing content with custom blocks removed to check if there are real differences.
|
|
91
|
+
* - All whitespace normalization from normalizeWhitespace
|
|
92
|
+
* - Removes ALL empty lines (only compares non-empty content)
|
|
93
|
+
* - Trims each line
|
|
94
|
+
*
|
|
95
|
+
* This aggressive approach is appropriate because when users add custom blocks,
|
|
96
|
+
* they often add empty lines before/after for formatting. These empty lines
|
|
97
|
+
* should not be considered "real differences" when the custom block is removed.
|
|
98
|
+
*/
|
|
99
|
+
function normalizeForSemanticComparison(content) {
|
|
100
|
+
const normalized = content
|
|
101
|
+
.replaceAll('\r\n', '\n')
|
|
102
|
+
.replaceAll('\n\r', '\n')
|
|
103
|
+
.replaceAll('\r', '\n')
|
|
104
|
+
.replaceAll('\uFEFF', '');
|
|
105
|
+
const lines = normalized.split('\n').map((line) => line.trimEnd());
|
|
106
|
+
// Keep only non-empty lines for comparison
|
|
107
|
+
const nonEmptyLines = lines.filter((line) => line.trim() !== '');
|
|
108
|
+
return nonEmptyLines.join('\n');
|
|
109
|
+
}
|
|
87
110
|
/**
|
|
88
111
|
* Checks if two arrays of lines differ only in whitespace/spacing
|
|
89
112
|
*/
|
|
@@ -95,11 +118,23 @@ function isWhitespaceOnlyDifference(sourceLines, incomingLines) {
|
|
|
95
118
|
/**
|
|
96
119
|
* Generates a merged output with conflict markers between two file contents.
|
|
97
120
|
*
|
|
121
|
+
* This function supports custom block preservation: if the source content contains
|
|
122
|
+
* custom blocks marked with `// @custom-start` and `// @custom-end` comments,
|
|
123
|
+
* those blocks will be automatically inserted into the generated content at their
|
|
124
|
+
* appropriate positions (based on anchor context), avoiding unnecessary merge conflicts
|
|
125
|
+
* for custom code that was intentionally added.
|
|
126
|
+
*
|
|
98
127
|
* @param contentSource - The content of the first file as a string.
|
|
99
128
|
* @param contentIncoming - The content of the second file as a string.
|
|
100
129
|
* @param labelSource - Optional label for the first file in conflict markers.
|
|
101
130
|
* @param labelIncoming - Optional label for the second file in conflict markers.
|
|
102
131
|
* @returns The merged content with conflict markers as a string.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* // Mark custom code in ejected files:
|
|
135
|
+
* // @custom-start:myFeature
|
|
136
|
+
* // your custom code here
|
|
137
|
+
* // @custom-end:myFeature
|
|
103
138
|
*/
|
|
104
139
|
function generateMergeConflict({ contentSource, contentIncoming, labelSource = 'Manual', labelIncoming = 'Generated', }) {
|
|
105
140
|
// In case the content is binary, we just return the incoming content
|
|
@@ -113,6 +148,92 @@ function generateMergeConflict({ contentSource, contentIncoming, labelSource = '
|
|
|
113
148
|
if (sourceNormalized === incomingNormalized) {
|
|
114
149
|
return contentIncoming;
|
|
115
150
|
}
|
|
151
|
+
// Handle custom blocks: extract from source, insert into incoming
|
|
152
|
+
if ((0, custom_blocks_1.hasCustomBlockMarkers)(contentSource)) {
|
|
153
|
+
return generateMergeConflictWithCustomBlocks({
|
|
154
|
+
contentSource,
|
|
155
|
+
contentIncoming,
|
|
156
|
+
labelSource,
|
|
157
|
+
labelIncoming,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// Standard merge conflict generation (no custom blocks)
|
|
161
|
+
return generateStandardMergeConflict({
|
|
162
|
+
contentSource,
|
|
163
|
+
contentIncoming,
|
|
164
|
+
labelSource,
|
|
165
|
+
labelIncoming,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Generates merge conflict output while preserving custom blocks.
|
|
170
|
+
*
|
|
171
|
+
* The algorithm:
|
|
172
|
+
* 1. Extract custom blocks from the source (manual) content
|
|
173
|
+
* 2. Compare source (minus custom blocks) with original incoming to check for real conflicts
|
|
174
|
+
* 3. If no real conflicts, insert custom blocks into incoming and return without conflict markers
|
|
175
|
+
* 4. If real conflicts exist, insert custom blocks into incoming and generate merge conflict
|
|
176
|
+
* between source (minus custom blocks) and modified incoming
|
|
177
|
+
*/
|
|
178
|
+
function generateMergeConflictWithCustomBlocks({ contentSource, contentIncoming, labelSource, labelIncoming, }) {
|
|
179
|
+
// Extract custom blocks from source
|
|
180
|
+
const extractResult = (0, custom_blocks_1.extractCustomBlocks)(contentSource);
|
|
181
|
+
if (extractResult.blocks.length === 0) {
|
|
182
|
+
// No valid custom blocks found, fall back to standard merge conflict
|
|
183
|
+
return generateStandardMergeConflict({
|
|
184
|
+
contentSource,
|
|
185
|
+
contentIncoming,
|
|
186
|
+
labelSource,
|
|
187
|
+
labelIncoming,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// Reconstruct source content without custom blocks
|
|
191
|
+
const sourceWithoutCustomBlocks = (0, custom_blocks_1.reconstructNonCustomContent)(extractResult);
|
|
192
|
+
// Compare source (minus custom blocks) with original incoming to check for real conflicts
|
|
193
|
+
// If they match, the only difference was the custom blocks, so no conflict needed
|
|
194
|
+
// Use semantic comparison which is more lenient about empty line differences
|
|
195
|
+
const sourceNormalized = normalizeForSemanticComparison(sourceWithoutCustomBlocks);
|
|
196
|
+
const incomingNormalized = normalizeForSemanticComparison(contentIncoming);
|
|
197
|
+
// Insert custom blocks into incoming content
|
|
198
|
+
const { content: incomingWithCustomBlocks, unplacedBlocks } = (0, custom_blocks_1.insertCustomBlocks)(extractResult.blocks, contentIncoming);
|
|
199
|
+
if (sourceNormalized === incomingNormalized) {
|
|
200
|
+
// No conflicts outside of custom blocks - just return incoming with custom blocks
|
|
201
|
+
return handleUnplacedBlocks(incomingWithCustomBlocks, unplacedBlocks);
|
|
202
|
+
}
|
|
203
|
+
// There are real conflicts outside of custom blocks
|
|
204
|
+
// Generate merge conflict between source (minus custom blocks) and incoming-with-custom-blocks
|
|
205
|
+
// This way, the custom blocks appear on the "Generated" side of any conflicts
|
|
206
|
+
const mergeResult = generateStandardMergeConflict({
|
|
207
|
+
contentSource: sourceWithoutCustomBlocks,
|
|
208
|
+
contentIncoming: incomingWithCustomBlocks,
|
|
209
|
+
labelSource,
|
|
210
|
+
labelIncoming,
|
|
211
|
+
});
|
|
212
|
+
return handleUnplacedBlocks(mergeResult, unplacedBlocks);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Appends unplaced custom blocks at the end of the content with a warning comment
|
|
216
|
+
*/
|
|
217
|
+
function handleUnplacedBlocks(content, unplacedBlocks) {
|
|
218
|
+
if (unplacedBlocks.length === 0) {
|
|
219
|
+
return content;
|
|
220
|
+
}
|
|
221
|
+
let result = content.trimEnd();
|
|
222
|
+
result += '\n\n';
|
|
223
|
+
result += '// ⚠️ WARNING: The following custom blocks could not be automatically placed.\n';
|
|
224
|
+
result += '// Please manually move them to the appropriate location.\n';
|
|
225
|
+
for (const block of unplacedBlocks) {
|
|
226
|
+
result += '\n';
|
|
227
|
+
result += `// --- Unplaced custom block${block.name ? `: ${block.name}` : ''} ---\n`;
|
|
228
|
+
result += block.lines.join('\n');
|
|
229
|
+
result += '\n';
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Standard merge conflict generation without custom block handling
|
|
235
|
+
*/
|
|
236
|
+
function generateStandardMergeConflict({ contentSource, contentIncoming, labelSource, labelIncoming, }) {
|
|
116
237
|
const blocks = new Blocks({ source: contentSource, incoming: contentIncoming });
|
|
117
238
|
let result = '';
|
|
118
239
|
for (const block of blocks.blocks) {
|
|
@@ -131,9 +252,13 @@ function generateMergeConflict({ contentSource, contentIncoming, labelSource = '
|
|
|
131
252
|
}
|
|
132
253
|
// Real content difference - create conflict markers
|
|
133
254
|
result += `${exports.mergeConflictMarkers.start} ${labelSource}\n`;
|
|
134
|
-
|
|
255
|
+
if (block.source.length > 0) {
|
|
256
|
+
result += block.source.join('\n') + '\n';
|
|
257
|
+
}
|
|
135
258
|
result += `${exports.mergeConflictMarkers.separator}\n`;
|
|
136
|
-
|
|
259
|
+
if (block.incoming.length > 0) {
|
|
260
|
+
result += block.incoming.join('\n') + '\n';
|
|
261
|
+
}
|
|
137
262
|
result += `${exports.mergeConflictMarkers.end} ${labelIncoming}\n`;
|
|
138
263
|
}
|
|
139
264
|
return result;
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export declare const zPosixPath: z.ZodBranded<z.ZodString, "PXL.PosixPath">;
|
|
2
|
+
export declare const zPosixPath: z.core.$ZodBranded<z.ZodString, "PXL.PosixPath", "out">;
|
|
3
3
|
/**
|
|
4
4
|
* A path string that has been normalized to use the unix path separator.
|
|
5
5
|
*/
|
|
6
6
|
export type PosixPath = z.infer<typeof zPosixPath>;
|
|
7
|
-
export declare const POSIX_ROOT: string & z.
|
|
7
|
+
export declare const POSIX_ROOT: string & z.core.$brand<"PXL.PosixPath">;
|
|
8
8
|
/**
|
|
9
9
|
* Normalizes a unix or Windows path to use the unix path separator. Additionally,
|
|
10
10
|
* it normalizes the path so that every path is considered absolute.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export declare const zTypescript: z.ZodBranded<z.ZodString, "PXL.Typescript">;
|
|
2
|
+
export declare const zTypescript: z.core.$ZodBranded<z.ZodString, "PXL.Typescript", "out">;
|
|
3
3
|
export type Typescript = z.infer<typeof zTypescript>;
|
|
4
|
-
export declare function ts(input: string): string & z.
|
|
4
|
+
export declare function ts(input: string): string & z.core.$brand<"PXL.Typescript">;
|
|
5
5
|
export declare function trimLines(input: string): string;
|
|
6
6
|
/**
|
|
7
7
|
* Removes leading and trailing newlines from the input string.
|
|
@@ -32,10 +32,10 @@ function logSyncResult(results, options = {}) {
|
|
|
32
32
|
actionGroups['Ejected'].push(path);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
logActionResult('NoAction', actionGroups['NoAction'], utils_1.cyan, 'unchanged', logger);
|
|
36
|
+
logActionResult('Write', actionGroups['Write'], utils_1.green, 'written:', logger);
|
|
35
37
|
logActionResult('Delete', actionGroups['Delete'], utils_1.red, 'deleted', logger);
|
|
36
38
|
logActionResult('MergeConflict', actionGroups['MergeConflict'], utils_1.yellow, 'with merge conflicts', logger);
|
|
37
|
-
logActionResult('Write', actionGroups['Write'], utils_1.green, 'written:', logger);
|
|
38
|
-
logActionResult('NoAction', actionGroups['NoAction'], utils_1.cyan, 'unchanged', logger);
|
|
39
39
|
if (options.showEjectedStats) {
|
|
40
40
|
if (actionGroups['Ejected'].length === 0) {
|
|
41
41
|
logger.log('✅ No files ejected');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postxl/generator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Core package that orchestrates the code generation of a PXL project",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"jszip": "3.10.1",
|
|
47
47
|
"minimatch": "^10.1.1",
|
|
48
48
|
"p-limit": "3.1.0",
|
|
49
|
-
"@postxl/schema": "^1.
|
|
50
|
-
"@postxl/utils": "^1.
|
|
49
|
+
"@postxl/schema": "^1.2.0",
|
|
50
|
+
"@postxl/utils": "^1.3.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/diff": "8.0.0"
|