@postxl/generator
v1.3.6
Published
Core package that orchestrates the code generation of a PXL project
Readme
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
ProjectSchemadefines the overall structure of the project, including the models, enums, generators, etc. - The
generateprogram that uses theProjectSchemato 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
// 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 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
| 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 |
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.jsonso 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:
// 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
}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
--forceflag is set). Resolve all conflicts before regenerating.
Force Regeneration
If you want to discard your changes and reset to the generated version:
# Force regenerate a specific file
pnpm run generate -f -p 'backend/libs/types/**/*.ts'
# Force regenerate everything (careful!)
pnpm run generate -fCustom 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'
// @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
}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
// 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 */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
// ... 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:orphanedFeatureBest Practices
- Use descriptive names:
// @custom-start:authMiddlewareis 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:foohas a matching@custom-end:foo
Example: Adding Custom Routes
// 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 routerWhen 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
# Standard generation
pnpm run generate
# Force regenerate all files (overwrites ejected files)
pnpm run generate -f
# Force regenerate specific files (glob pattern)
pnpm run generate -f -p 'backend/libs/types/**/*.ts'
# Show ejected files after generation
pnpm run generate -e
# Show diff between ejected and generated versions
pnpm run generate -d
# Watch mode - regenerate on schema changes
pnpm run generate:watch
# Skip linting and formatting
pnpm run generate -tTroubleshooting
"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:
# Regenerate everything (preserves ejected files unless they conflict)
pnpm run generate
# Or force regenerate to reset lock file
pnpm run generate -f