Skip to Content
FrontendCrud V2Crud GeneratorGenerator v2TSX Generation Internals

TSX Generation Internals

generateTsx() in src/tsx/tsx-generator.js converts a JSON layer version into a typed index.tsx file. This page documents every significant step, internal helper, and edge-case behaviour.


The 10-Step Pipeline

gen:tsx 1. Resolve JSON source version (--jsonVersion or latest) 2. Load data.json + changes.json 3. Determine next TSX version, create directory 4. Copy config-template.tsx → index.tsx 5. Normalize all field sets to { fieldName: metadata } objects 6. Apply fieldOverrides from changes.json 7. Apply exclusions, hide lists, order, writeableProperties filtering 8. Call all template-generator AddX() functions to populate placeholders 9. applyChangesCustomizations() — regex post-processing 10. Format with Prettier

Step 5 — Field Normalization

normalizeToFieldObject(data)

All field data — whether it arrived from an old array format or the current object format — is normalised to a single shape before any processing:

{ [fieldName: string]: { fieldName: string, type: string, ... } }
Input shapeHow it’s handled
{ fieldName: metadata, … } (current raw API format)Each entry enriched with enrichFieldWithResourceInfo()
[{ fieldName, type, … }, …] (old array format)Indexed by fieldName
{ "Section": [field, …], … } (sectioned legacy format)Sections flattened, indexed by fieldName
["fieldName", …] (string array)Each string becomes { fieldName, type: 'string' }

normalizeRequiredFields(requiredFields, allFields)

Required fields may arrive as either a string array (["name", "type"]) or a full field-object array. In the string case, metadata is looked up from allFields — or a minimal stub { fieldName, type: 'string', required: true } is created.

enrichFieldWithResourceInfo(field)

Relation fields (MANY_TO_ONE, ONE_TO_ONE, etc.) need targetEntityResourceName and targetEntityResourceIri for the SELECT_ASYNC component to make API calls. If these are missing from the raw API data, they are derived automatically:

field.targetEntity = "AccountingBundle/AccountType" ↓ deriveResourceNameFromEntity() targetEntityResourceName = "accounting_account_types" targetEntityResourceIri = "/api/accounting_account_types"

This derivation only runs when targetEntity is present but the resource fields are absent. If the API already returned them, they are used as-is.


Step 6 — fieldOverrides

changes.fieldOverrides is applied to allAvailableFields — a merged pool of every field from every source (listing, show, form, filter, required). This means overrides affect the field regardless of which section it appears in.

"fieldOverrides": { "price": { "type": "CURRENCY" }, "tableName": { "optionsArrayFinite": "TableNames" } }

If the field does not exist in allAvailableFields at all, it is created as a new entry from the override object — useful for injecting synthetic fields.


Step 7 — Field Filtering & Ordering

isFieldHidden(fieldName, section) — exclusion category mapping

Each changes.json section maps to a specific exclusionList category:

Section keyexclusionList category checked
listingFieldslisting
formFieldsforms
showFieldsshow
filterFieldslisting
formModalShowFieldsforms
formModalEditFieldsforms
formModalAddFieldsforms

A field is hidden if it appears in either changes[section].hide or changes.exclusionList[category].

For modal sections (formModalShow/Edit/Add), changes.formFields.hide is also checked — hiding a form field hides it from all modals too.


listingFields.hideSelected

listingFields.selected controls what’s shown in the listing. listingFields.hideSelected is a sub-list of fields that are selected (counted in the column set) but not visible by default in the user-facing column toggle. They are present in the data but hidden in the UI toggle.

"listingFields": { "selected": ["id", "name", "status", "internalRef"], "hideSelected": ["internalRef"] }

internalRef is in the column set but won’t appear in the user’s column-visibility toggle.


writeableProperties — three distinct states

writeableProperties comes from the API’s writable_properties field and controls which fields appear in forms and modals.

StateValueBehaviour
Not returned by APInull / undefinedSkip whitelist — all form fields included
Returned as empty array[]Entity is explicitly read-only — form fields cleared to {}
Returned as populated array["name", "type", …]Whitelist — only listed fields kept in formFields, formModalEditFields, formModalAddFields, section maps

The null vs [] distinction matters. An API that returns writable_properties: [] explicitly signals a read-only entity. The generator will produce empty form inputs in that case.

formModalShowFields (read-only view modal) is not filtered by writeableProperties — it’s for display only.


sortableFields — auto-applied from API

If the API returns OrderFilter in apiFilters, the generator extracts its gobal.properties list and uses it as the default sortable list for listingFields, unless changes.listingFields.sortable is already set explicitly.


Form sections + writeableProperties interaction

When both formFields.sections and writeableProperties are present, sections are filtered field-by-field. Any section whose entire field list is excluded ends up empty and is dropped from the output.


Step 8 — Template Function Calls

The template functions are called in this order on index.tsx:

FunctionReplaces placeholderInput
HeaderInformation[generationDate]Current timestamp
AddFQCN[bundle], [unit]Encoded entity + bundleCrud
AddResource[resource], [tableName]resourceName + tableName from JSON
AddFieldsAll[fieldsAll]All listing column keys
AddFieldsExport[fieldsExport]Same as fieldsAll
AddSelectedList[selectedList]Default-selected listing fields
AddMainColumns[mainColumns]Listing fields with listingConfig (sortable, widths, alignment, iconFields)
AddFormModalInputs (×3)[formModalShowInputs], [formModalEditInputs], [formModalAddInputs]Per-modal field set
AddFormInputs[addFormInputs]Form fields with section map and required set
AddImportStatements[importStatements]Extra imports for special entities
AddFilterFields[addFilterFields]Filter fields with filter config
AddSideBoxs[addSideBoxs]Sidebox relationship fields
AddDisplayFields[addDisplayFields]Show-page field display with section map
AddqField[qfield]Primary search field from encoded entity
AddInterfaces[implementedInterfaces]Interface map
AddDefaultListingFilters[defaultFilters]defaultFilterValues from filterFields changes
DefaultListingActions[defaultListingActions]Default actions per bundleCrud + interfaces
AllListingActions[listingAllActions], [kanbanListingAllActions], [treeviewListingAllActions]From changes.actions.*
DefaultCustomActions[customActions]Actions from resourceName + interfaces

formModalFields fallback chain

The three typed modal sections (formModalShowFields, formModalEditFields, formModalAddFields) were introduced after the legacy formModalFields key. The resolver uses:

const sectionChanges = changes[sectionKey] ?? changes.formModalFields ?? {};

So if a changes.json only has the old formModalFields key, all three modal types inherit from it. If a typed key exists, it takes precedence.


Step 9 — Post-Processing (applyChangesCustomizations)

After all AddX() calls, the generated file is read back as a string and a set of regex replacements are applied. This step allows changes.json to directly override values that were already written into the TypeScript source by the template functions.

What’s patched via regex

changes.json keyWhat gets patched in the TSX
listingFields.selectedRewrites const selectedList: string[] = [...]
filterFields.defaultsSets default: true/false on each filter field object
formFields.required / optionalSets required: true/false on each form input object
actions.listingRewrites const defaultActions: string[] = [...]
actions.listingAllActionsRewrites const listingAllActions: string[] = [...]
actions.kanbanListingAllActionsRewrites const kanbanListingAllActions: string[] = [...]
actions.treeviewListingAllActionsRewrites const treeviewListingAllActions: string[] = [...]
actions.showPageRewrites const showPageActions: string[] = [...]

This two-phase approach (template functions → regex patch) means that some values are set twice — once by the template generator based on raw field data, then overridden in the post-processing step based on your changes.json. The regex patch is the final value.

Boolean fields always non-required

A hardcoded workaround forces all ColumnTypeEnum.BOOLEAN fields to required: false, regardless of what the API or changes.json specifies. This is applied globally after all other processing.

// TEMPORARY WORKAROUND // https://github.com/PHPCreation/.../issues/838 content = content.replace( /(type:\s*ColumnTypeEnum\.BOOLEAN[^}]*?required:\s*)true/g, '$1false' );

Relation Field Auto-Detection

Fields with relational types (MANY_TO_ONE, ONE_TO_MANY, MANY_TO_MANY, ONE_TO_ONE) receive ColumnTypeEnum.SELECT_ASYNC automatically. Additionally:

  • MANY_TO_MANY fields are automatically excluded from form inputs and all modal inputs — they appear only in sideboxes
  • targetEntityResourceIri is set to /api/{resourceName} for use as the async options endpoint

allAvailableFields Pool

A merged map of all fields from all sources (listing + show + form + required + filter) is built before filtering and used as the lookup for every section. This means:

  • A field in showFields can be referenced in formFields.sections
  • fieldOverrides applies universally
  • toString is always injected as a virtual field: { fieldName: 'toString', type: 'string', format: 'string' }

generateTsxAll(options)

Calls MetaDataEntitiesList to enumerate all entities in the bundle, then runs generateTsx for each one sequentially. Entity names are decoded from URL-encoded PHP FQCN format back to Bundle/Entity format before each call.

Returns { success: [], failed: [], skipped: [] }.

Last updated on