Core package that orchestrates the code generation of a PXL project
npm install @postxl/generatorThe core package that orchestrates the code generation of a PXL project
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.
The generate program is composed of multiple generators.
Here is a simplified example program:
``ts
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
// some global generators (API, E2E, CmdK, ?) register some "collectors" in the context,
// so other generator can store data in these collectors.
// Upon generation, these global generators use this data to generator the code.
// Example: E2E Selector collector: frontend generators provide selectors that will be
// used by the E2E generators to generate the selectors object
.register(registerApiContext)
// these generators will create the relevant files in the virtual file system and provide more context
// to subsequent generators
.generate(generateTypes)
.generate(generateRepositories)
// this will generate the API code using the registered collector
.generate(generateApi)
// prettify files, add disclaimer, calculate checksums and write files to disk
// that have not been ejected
.flush()
}
`
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
The generator uses a 3-way sync algorithm to intelligently handle file changes. The three sources are:
1. Virtual File System (VFS) - The newly generated content
2. Lock File - Hash values from the previous generation run (postxl-lock.json)
3. Disk - The actual files on the filesystem
| Generated | Lock File | Disk | Action | Description |
| --------- | --------- | -------- | ------------------ | -------------------------------------------------------- |
| ✓ 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 |
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
When both you and the generator have made changes to the same file, the generator creates Git-style merge conflict markers:
`typescript
// Unchanged code stays clean
import { Injectable } from '@nestjs/common'
@Injectable()
export class UserService {
<<<<<<< Manual
// Your manual changes appear here
findAll() {
return this.customLogic()
}
=======
// Generated version appears here
findAll() {
return this.repository.findAll()
}
>>>>>>> Generated
}
`
1. Open the file in your editor
2. Decide which version to keep (or combine both)
3. Remove the conflict markers (<<<<<<<, =======, >>>>>>>)
4. 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.
If you want to discard your changes and reset to the generated version:
`bashForce regenerate a specific file
pnpm run generate -f -p 'backend/libs/types/*/.ts'
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.
$3
`typescript
import { Injectable } from '@nestjs/common'// @custom-start:imports
import { CustomLogger } from './logger'
import { MetricsService } from './metrics'
// @custom-end:imports
@Injectable()
export class UserService {
constructor(
private readonly repository: UserRepository,
// @custom-start:dependencies
private readonly logger: CustomLogger,
private readonly metrics: MetricsService,
// @custom-end:dependencies
) {}
// Generated methods...
findAll() {
return this.repository.findAll()
}
// @custom-start:customMethods
async findAllWithMetrics() {
this.metrics.increment('user.findAll')
return this.findAll()
}
async customBusinessLogic() {
this.logger.log('Custom logic executed')
return 'custom result'
}
// @custom-end:customMethods
}
`$3
When the generator runs and detects custom block markers in an ejected file:
1. Extract: Custom blocks are identified and extracted from your modified file
2. Compare: The remaining code (minus custom blocks) is compared to the new generated output
3. Reinsert: Custom blocks are automatically inserted back into the generated output at the same relative position
4. Conflict only if needed: Only actual changes outside your custom blocks will show merge conflict markers
$3
`typescript
// Line comment style (recommended)
// @custom-start:blockName
// ... your custom code ...
// @custom-end:blockName// Unnamed blocks (works, but names help with clarity)
// @custom-start
// ... your custom code ...
// @custom-end
// Block comment style (for languages that prefer it)
/ @custom-start:blockName /
/ ... your custom code ... /
/ @custom-end:blockName /
`$3
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$3
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
$3
If the generator cannot find a suitable position for a custom block (e.g., the surrounding code changed significantly), it will:
1. Append the block at the end of the file
2. Add a warning comment so you know to move it manually
`typescript
// ... rest of file ...// ⚠️ WARNING: The following custom blocks could not be automatically placed.
// Please manually move them to the appropriate location.
// --- Unplaced custom block: orphanedFeature ---
// @custom-start:orphanedFeature
// This code needs to be moved manually
// @custom-end:orphanedFeature
`$3
1. Use descriptive names:
// @custom-start:authMiddleware is better than // @custom-start
2. Keep blocks focused: One feature per block makes them easier to manage
3. Place strategically: Put blocks after stable anchor points
4. Don't nest blocks: Nested custom blocks are not supported
5. Match names: Ensure @custom-start:foo has a matching @custom-end:foo$3
`typescript
// Generated router file
import { Router } from 'express'
import { getUsers, getUserById, createUser } from './handlers'const router = Router()
// Generated routes
router.get('/users', getUsers)
router.get('/users/:id', getUserById)
router.post('/users', createUser)
// @custom-start:customRoutes
// Custom export endpoint
router.get('/users/export', async (req, res) => {
const users = await exportUsersToCSV()
res.attachment('users.csv').send(users)
})
// Custom bulk operations
router.post('/users/bulk', bulkCreateUsers)
router.delete('/users/bulk', bulkDeleteUsers)
// @custom-end:customRoutes
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
`bash
Standard generation
pnpm run generateForce regenerate all files (overwrites ejected files)
pnpm run generate -fForce regenerate specific files (glob pattern)
pnpm run generate -f -p 'backend/libs/types/*/.ts'Show ejected files after generation
pnpm run generate -eShow diff between ejected and generated versions
pnpm run generate -dWatch mode - regenerate on schema changes
pnpm run generate:watchSkip linting and formatting
pnpm run generate -t
`Troubleshooting
$3
The generator found files with conflict markers (
<<<<<<<, =======, >>>>>>>). Resolve these manually before running the generator again.$3
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.
$3
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$3
If
postxl-lock.json gets out of sync with your files:`bash
Regenerate everything (preserves ejected files unless they conflict)
pnpm run generateOr force regenerate to reset lock file
pnpm run generate -f
``