PXL Generator
The core package that orchestrates the code generation of a PXL project
Overview & context
Each PXL project is initially generated using two components:
- The
ProjectSchema defines the overall structure of the project, including the models, enums, generators, etc.
- The
generate program that uses the ProjectSchema to generate the code for the project.
The ProjectSchema is defined and explained in the Schema package.
This package provides the tooling for the generate program.
Composable generators
The generate program is composed of multiple generators.
Here is a simplified example program:
import { Generator } from '@postxl/generator'
import { generateTypes } from '@postxl/generators/types'
import { registerApiContext, generateApi}* from '@postxl/generators/nestjs-backend'
import { generateRepositories } from '@postxl/generators/prisma-repositories'
import { zProjectSchema } from '@postxl/schema'
import { projectSchemaJSON } from './project-schema.json'
async function generateProject() {
const projectSchema = zProjectSchema.parse(projectSchemaJSON)
const generator = new Generator(projectSchema)
await generator
.register(registerApiContext)
.generate(generateTypes)
.generate(generateRepositories)
.generate(generateApi)
.flush()
}
Running the generator and updating the result on disk
Running the generator will create the code of the project, given the ProjectSchema and the generator configs.
From there, two things will happen over time:
- The models (or generator configs) will change,
- The (initially generated) code will be manually changed (formerly known as "ejected")
In case the file was not manually changed but the model was changed, the generator will
automatically update the file on disk. In case the file was manually changed and the model
was not changed, nothing will happen. Only in case the file was manually changed and the model
was changed, we will need to decide how to handle the conflict. For this, the generator will
update the existing file in a way that is compatible with a git merge conflict. This way, the
developer must decide how to resolve the conflict.
Under the hood, the above logic is implemented leveraging the "postxl-lock.json" file.
This file contains the hash values for each generated file. With this hash value, we can determine:
- If the file was manually changed: in this case the hash of the current file will be different from
the hash in the lock file
- If the file was changed by the latest generator run: in this case the hash of the newly generated
file will be different from the hash in the lock file
File Sync Algorithm
The generator uses a 3-way sync algorithm to intelligently handle file changes. The three sources are:
- Virtual File System (VFS) - The newly generated content
- Lock File - Hash values from the previous generation run (
postxl-lock.json)
- Disk - The actual files on the filesystem
State Matrix
| ✓ Changed | Same | Modified | Merge Conflict | File was ejected AND generator template changed |
| ✓ Changed | Same | Same | Write | Template changed, file not ejected → auto-update |
| ✓ Same | Same | Modified | No Action | File ejected, but template unchanged → keep your changes |
| ✓ Same | Same | Same | No Action | Nothing changed |
| ✓ New | - | Exists | Merge Conflict | New generated file conflicts with existing file |
| ✓ New | - | - | Write | Brand new file |
| - Removed | Exists | Modified | Delete | Generator no longer produces this file |
Ejected Files
A file is considered "ejected" when you manually modify it. Once ejected:
- The generator will not overwrite your changes automatically
- If the generator template changes, you'll get a merge conflict to resolve
- The file remains tracked in
postxl-lock.json so the generator knows it exists
Merge Conflicts
When both you and the generator have made changes to the same file, the generator creates Git-style merge conflict markers:
import { Injectable } from '@nestjs/common'
@Injectable()
export class UserService {
<<<<<<< Manual
findAll() {
return this.customLogic()
}
=======
findAll() {
return this.repository.findAll()
}
>>>>>>> Generated
}
Resolving Merge Conflicts
- Open the file in your editor
- Decide which version to keep (or combine both)
- Remove the conflict markers (
<<<<<<<, =======, >>>>>>>)
- Run the generator again to verify
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.
Force Regeneration
If you want to discard your changes and reset to the generated version:
pnpm run generate -f -p 'backend/libs/types/**/*.ts'
pnpm run generate -f
Custom Block Preservation
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.
Basic Usage
import { Injectable } from '@nestjs/common'
import { CustomLogger } from './logger'
import { MetricsService } from './metrics'
@Injectable()
export class UserService {
constructor(
private readonly repository: UserRepository,
private readonly logger: CustomLogger,
private readonly metrics: MetricsService,
) {}
findAll() {
return this.repository.findAll()
}
async findAllWithMetrics() {
this.metrics.increment('user.findAll')
return this.findAll()
}
async customBusinessLogic() {
this.logger.log('Custom logic executed')
return 'custom result'
}
}
How It Works
When the generator runs and detects custom block markers in an ejected file:
- Extract: Custom blocks are identified and extracted from your modified file
- Compare: The remaining code (minus custom blocks) is compared to the new generated output
- Reinsert: Custom blocks are automatically inserted back into the generated output at the same relative position
- Conflict only if needed: Only actual changes outside your custom blocks will show merge conflict markers
Marker Syntax
Block Names
Names are optional but recommended when you have multiple custom blocks:
- Must be alphanumeric with hyphens/underscores:
[a-zA-Z0-9_-]+
- Help identify blocks in warnings
- Opening and closing names should match
Anchor-Based Positioning
Custom blocks are repositioned based on anchor context - the significant code lines immediately before and after your block. For best results:
- Place custom blocks after stable, identifiable lines (method signatures, class declarations, import statements)
- Avoid placing blocks in areas that frequently change
- The more unique the surrounding context, the more reliable the repositioning
When Blocks Cannot Be Placed
If the generator cannot find a suitable position for a custom block (e.g., the surrounding code changed significantly), it will:
- Append the block at the end of the file
- Add a warning comment so you know to move it manually
Best Practices
- Use descriptive names:
// @custom-start:authMiddleware is better than // @custom-start
- Keep blocks focused: One feature per block makes them easier to manage
- Place strategically: Put blocks after stable anchor points
- Don't nest blocks: Nested custom blocks are not supported
- Match names: Ensure
@custom-start:foo has a matching @custom-end:foo
Example: Adding Custom Routes
import { Router } from 'express'
import { getUsers, getUserById, createUser } from './handlers'
const router = Router()
router.get('/users', getUsers)
router.get('/users/:id', getUserById)
router.post('/users', createUser)
router.get('/users/export', async (req, res) => {
const users = await exportUsersToCSV()
res.attachment('users.csv').send(users)
})
router.post('/users/bulk', bulkCreateUsers)
router.delete('/users/bulk', bulkDeleteUsers)
export default router
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).
CLI Options
pnpm run generate
pnpm run generate -f
pnpm run generate -f -p 'backend/libs/types/**/*.ts'
pnpm run generate -e
pnpm run generate -d
pnpm run generate:watch
pnpm run generate -t
Troubleshooting
"Unresolved merge conflicts detected"
The generator found files with conflict markers (<<<<<<<, =======, >>>>>>>). Resolve these manually before running the generator again.
Custom blocks appearing at end of file
The generator couldn't find the anchor context for your block. This happens when:
- The code before/after your block changed significantly
- The block was placed in a frequently-changing area
Solution: Move the block back to its correct position and ensure it has stable anchor lines nearby.
Unexpected merge conflicts in custom block areas
If you're seeing conflicts around custom blocks, check:
- Block markers are properly formatted (
@custom-start/@custom-end)
- Names match between start and end markers
- No nested custom blocks
Lock file out of sync
If postxl-lock.json gets out of sync with your files:
pnpm run generate
pnpm run generate -f