This document describes the implementation of the extension system for ODE core, allowing custom apps (e.g., AnthroCollect) to define their own extensions without ODE core owning or defining them.
The extension system follows a three-tier architecture:
- Formulus (React Native) - Extension discovery and loading
- Synkronus (Go) - Extension validation during bundle upload
- Formplayer (WebView) - Runtime extension registration
File: formulus/src/services/ExtensionService.ts (NEW)
- Implements
getCustomAppExtensions()API - Discovers
ext.jsonfiles in:/forms/ext.json(app-level)/forms/{formName}/ext.json(form-level)
- Merges extensions with precedence: form-level → app-level → core defaults
- Returns normalized extension object (definitions, functions, renderers)
File: formulus/src/services/index.ts (MODIFIED)
- Exports
ExtensionService
File: formulus/src/components/FormplayerModal.tsx (MODIFIED)
- Loads extensions using
ExtensionServicewhen initializing forms - Passes extension metadata to formplayer via
FormInitData
File: formulus/src/webview/FormulusInterfaceDefinition.ts (MODIFIED)
- Added
ExtensionMetadatainterface - Extended
FormInitDatato include optionalextensionsfield
File: formulus-formplayer/src/extensionLoader.ts (NEW)
- Dynamically imports renderer components and tester functions
- Loads custom functions
- Handles import failures gracefully with error reporting
File: formulus-formplayer/src/App.tsx (MODIFIED)
- Calls
loadExtensions()when extensions are provided - Registers extension renderers with JsonForms (highest priority)
- Adds extension definitions to AJV for
$refsupport - Handles missing renderers and import failures
File: formulus-formplayer/src/FormulusInterfaceDefinition.ts (MODIFIED)
- Added
ExtensionMetadatainterface matching Formulus definition
File: synkronus-cli/pkg/validation/bundle.go (MODIFIED)
- Added
validateExtensions()function - Validates
ext.jsonstructure and required fields - Checks that extension modules exist in bundle
- Validates UI schema format references against extension renderers
- Validates schema format properties against extension renderers
- Provides actionable error messages
Extensions are defined in ext.json files with the following structure:
{
"definitions": {
"CustomType": {
"type": "object",
"properties": {
"field": { "type": "string" }
}
}
},
"functions": {
"customFunction": {
"name": "customFunction",
"module": "utils/customFunction.js",
"export": "default"
}
},
"renderers": {
"customRenderer": {
"name": "CustomRenderer",
"format": "custom-format",
"module": "renderers/CustomRenderer.tsx",
"tester": "customRendererTester",
"renderer": "CustomRenderer"
}
}
}Extensions are merged with the following precedence (highest to lowest):
- Form-level (
/forms/{formName}/ext.json) - Highest priority - App-level (
/forms/ext.json) - Medium priority - Core defaults - Lowest priority (built-in renderers, formats)
The Synkronus validator enforces:
-
Extension File Structure
- Must be valid JSON
- Required fields:
name,format,modulefor renderers - Required fields:
namefor functions
-
Module Existence
- Extension modules must exist in bundle (or be valid module paths)
- Modules can be in
forms/orapp/directories
-
Renderer References
- UI schema format properties must have corresponding extension renderer
- Schema format properties must have corresponding extension renderer
- Built-in formats are allowed (date, date-time, time, photo, qrcode, etc.)
-
Error Messages
- Fail early with actionable error messages
- Clear indication of which file/field caused the error
- Formulus loads extensions when initializing a form
- Extension metadata is passed to formplayer via
FormInitData - Formplayer dynamically imports extension modules
- Extension renderers are registered with JsonForms (highest priority)
- Extension definitions are added to AJV for
$refsupport
- Missing renderer: Clear runtime error logged, form continues without extension
- Import failure: Dev warning logged, safe fallback (form continues)
- Invalid extension: Validation fails during bundle upload (before deployment)
- ✅ No app logic moved into ODE core
- ✅ No hard-coded AnthroCollect paths
- ✅ No use of
eval - ✅ Backwards compatible (extensions are optional)
- ✅ Declarative only (no JS module imports in ExtensionService)
To test the extension system:
- Create an
ext.jsonfile in/forms/ext.jsonor/forms/{formName}/ext.json - Define a custom renderer with a unique format
- Reference the format in a form's UI schema
- Bundle and upload to Synkronus
- Validator should check extension structure and renderer references
- Formplayer should dynamically load and register the extension
{
"renderers": {
"customDate": {
"name": "CustomDateRenderer",
"format": "custom-date",
"module": "renderers/CustomDateRenderer.tsx",
"tester": "customDateTester",
"renderer": "CustomDateRenderer"
}
}
}Corresponding UI schema:
{
"type": "Control",
"scope": "#/properties/dateField",
"options": {
"format": "custom-date"
}
}- Extension modules use ES6 module syntax (
import/export) - Renderer modules export both a tester function and renderer component
- Function modules export the function as default or named export
- Module paths are relative to the custom app root
- WebView can access extension modules via
file://URLs
- Should extension functions be injected into a global evaluation context?
- How should extension functions be called from form validation rules?
- Should there be a versioning system for extensions?
- How should extension conflicts be resolved (same format, different renderers)?
- Test with AnthroCollect extensions
- Document extension development guide
- Add extension examples to documentation
- Consider extension versioning strategy