UNPKG

@tensorify.io/sdk

Version:

TypeScript SDK for developing Tensorify plugins with comprehensive validation, frontend enforcement, and publishing tools

545 lines 20.8 kB
"use strict"; /** * TensorifyPlugin - The core abstract class for all Tensorify plugins * * This is the single class that all plugin developers must extend. * It enforces frontend requirements, provides validation, and handles * manifest generation for the CLI publishing system. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TensorifyPlugin = void 0; const core_1 = require("../types/core"); const visual_1 = require("../types/visual"); const settings_1 = require("../types/settings"); /** * Abstract base class for all Tensorify plugins * * Every plugin must extend this class and implement the required methods. * This class enforces frontend visual requirements and provides utilities * for plugin development. */ class TensorifyPlugin { /** Plugin definition containing all configuration */ definition; /** Current SDK version */ static SDK_VERSION = "1.0.0"; /** Manifest format version */ static MANIFEST_VERSION = "1.0.0"; /** * Constructor - Creates a new plugin instance * @param definition Complete plugin definition */ constructor(definition) { this.definition = definition; this.validateDefinition(); } // ======================================== // INPUT ACCESS HELPER // ======================================== /** * Helper method to access input data from handles * * @param context Code generation context * @param handleNumber Handle index (0-based) * @returns Input data from the specified handle */ getInput(context, handleNumber) { return context?.inputData[handleNumber] || null; } /** * Get all input data as an array * * @param context Code generation context * @returns Array of all input data */ getAllInputs(context) { if (!context?.inputData) return []; const maxHandle = Math.max(...Object.keys(context.inputData).map(Number)); const inputs = []; for (let i = 0; i <= maxHandle; i++) { inputs.push(context.inputData[i] || null); } return inputs; } // ======================================== // PLUGIN DEFINITION ACCESSORS // ======================================== /** Get plugin ID (may be undefined if not provided in definition) */ getId() { return this.definition.id; } /** Get plugin name (may be undefined if not provided in definition) */ getName() { return this.definition.name; } /** Get plugin description (may be undefined if not provided in definition) */ getDescription() { return this.definition.description; } /** Get plugin version (may be undefined if not provided in definition) */ getVersion() { return this.definition.version; } /** Get node type (may be undefined if not provided in definition) */ getNodeType() { return this.definition.nodeType; } /** Get visual configuration */ getVisualConfig() { return this.definition.visual; } /** Get input handles */ getInputHandles() { return this.definition.inputHandles; } /** Get output handles */ getOutputHandles() { return this.definition.outputHandles; } /** Get settings fields */ getSettingsFields() { return this.definition.settingsFields; } /** Get settings groups */ getSettingsGroups() { return this.definition.settingsGroups || []; } /** Get capabilities */ getCapabilities() { return this.definition.capabilities; } /** Get requirements */ getRequirements() { return this.definition.requirements; } /** Get complete definition */ getDefinition() { return { ...this.definition }; } // ======================================== // VALIDATION METHODS // ======================================== /** * Validate plugin settings before code generation * * @param settings Settings to validate * @returns Validation result */ validateSettings(settings) { const errors = []; const warnings = []; if (!settings.labelName) { errors.push({ type: "missing_property", message: "labelName is required in plugin settings", path: "labelName", expected: "string", actual: settings.labelName, }); } // Validate each settings field for (const field of this.definition.settingsFields) { const value = settings[field.key]; // Check required fields if (field.required && (value === undefined || value === null || value === "")) { errors.push({ type: "missing_property", message: `Required field '${field.key}' is missing`, path: field.key, expected: field.dataType, actual: value, }); continue; } // Skip validation if field is not required and not provided if (value === undefined || value === null) continue; // Validate data type if (!this.validateFieldDataType(value, field.dataType, field.key)) { errors.push({ type: "invalid_type", message: `Invalid type for field '${field.key}'`, path: field.key, expected: field.dataType, actual: typeof value, }); } // Validate field-specific rules if (field.validation) { const fieldErrors = this.validateFieldRules(value, field); errors.push(...fieldErrors); } } return { isValid: errors.length === 0, errors, warnings, }; } /** * Validate the plugin definition structure */ validateDefinition() { const errors = []; // Note: All core metadata (including nodeType) can be derived from package.json // Only visual configuration is truly required at definition time // Validate visual config if (!this.definition.visual) { errors.push("Plugin visual configuration is required"); } else { this.validateVisualConfig(errors); } // Validate handles this.validateHandles(errors); // Validate settings fields this.validateSettingsFields(errors); if (errors.length > 0) { throw new Error(`Plugin definition validation failed:\n${errors.join("\n")}`); } } /** * Validate visual configuration */ validateVisualConfig(errors) { const visual = this.definition.visual; if (!visual.containerType) { errors.push("Visual containerType is required"); } if (!visual.size || !visual.size.width || !visual.size.height) { errors.push("Visual size (width and height) is required"); } if (visual.size.width < 50 || visual.size.height < 30) { errors.push("Visual size too small (minimum 50x30)"); } } /** * Validate handle configurations */ validateHandles(errors) { const inputIds = new Set(); const outputIds = new Set(); let hasPrev = false; let hasNext = false; // Validate input handles for (const handle of this.definition.inputHandles) { if (!handle.id) { errors.push("Input handle id is required"); continue; } if (inputIds.has(handle.id)) { errors.push(`Duplicate input handle id: ${handle.id}`); } inputIds.add(handle.id); if (!handle.position) { errors.push(`Input handle ${handle.id} position is required`); } if (!handle.viewType) { errors.push(`Input handle ${handle.id} viewType is required`); } if (handle.id === "prev") { hasPrev = true; if (handle.position !== visual_1.HandlePosition.LEFT) { errors.push("Input handle 'prev' must be on the LEFT side"); } // prev is required for flow, but Start nodes may omit. Since SDK doesn't know node type here, // enforce required flag present and true to make UI render a required badge. if (handle.required !== true) { errors.push("Input handle 'prev' must be required: true"); } } } // Validate output handles for (const handle of this.definition.outputHandles) { if (!handle.id) { errors.push("Output handle id is required"); continue; } if (outputIds.has(handle.id)) { errors.push(`Duplicate output handle id: ${handle.id}`); } outputIds.add(handle.id); if (!handle.position) { errors.push(`Output handle ${handle.id} position is required`); } if (!handle.viewType) { errors.push(`Output handle ${handle.id} viewType is required`); } if (handle.id === "next") { hasNext = true; if (handle.position !== visual_1.HandlePosition.RIGHT) { errors.push("Output handle 'next' must be on the RIGHT side"); } } } // Enforce presence of prev/next handles if (!hasPrev) { errors.push("Plugin must define an input handle with id 'prev'"); } if (!hasNext) { errors.push("Plugin must define an output handle with id 'next'"); } } /** * Validate settings fields configuration */ validateSettingsFields(errors) { const fieldKeys = new Set(); for (const field of this.definition.settingsFields) { if (!field.key) { errors.push("Settings field key is required"); continue; } if (fieldKeys.has(field.key)) { errors.push(`Duplicate settings field key: ${field.key}`); } fieldKeys.add(field.key); if (!field.label) { errors.push(`Settings field ${field.key} label is required`); } if (!field.type) { errors.push(`Settings field ${field.key} type is required`); } if (!field.dataType) { errors.push(`Settings field ${field.key} dataType is required`); } // Validate type compatibility if (field.type && field.dataType) { const compatibleTypes = settings_1.UI_TYPE_TO_DATA_TYPE_MAP[field.type]; if (compatibleTypes && !compatibleTypes.includes(field.dataType)) { errors.push(`Settings field ${field.key} has incompatible type/dataType combination`); } } } } /** * Validate individual field data type */ validateFieldDataType(value, expectedType, fieldKey) { switch (expectedType) { case settings_1.SettingsDataType.STRING: return typeof value === "string"; case settings_1.SettingsDataType.NUMBER: return typeof value === "number" && !isNaN(value); case settings_1.SettingsDataType.BOOLEAN: return typeof value === "boolean"; case settings_1.SettingsDataType.ARRAY: return Array.isArray(value); case settings_1.SettingsDataType.OBJECT: return (typeof value === "object" && value !== null && !Array.isArray(value)); default: return true; } } /** * Validate field-specific rules */ validateFieldRules(value, field) { const errors = []; const validation = field.validation; if (typeof value === "string") { if (validation.minLength !== undefined && value.length < validation.minLength) { errors.push({ type: "invalid_value", message: `Field '${field.key}' is too short (minimum ${validation.minLength} characters)`, path: field.key, expected: `length >= ${validation.minLength}`, actual: value.length, }); } if (validation.maxLength !== undefined && value.length > validation.maxLength) { errors.push({ type: "invalid_value", message: `Field '${field.key}' is too long (maximum ${validation.maxLength} characters)`, path: field.key, expected: `length <= ${validation.maxLength}`, actual: value.length, }); } if (validation.pattern) { const regex = new RegExp(validation.pattern); if (!regex.test(value)) { errors.push({ type: "invalid_value", message: `Field '${field.key}' does not match required pattern`, path: field.key, expected: validation.pattern, actual: value, }); } } } if (typeof value === "number") { if (validation.min !== undefined && value < validation.min) { errors.push({ type: "invalid_value", message: `Field '${field.key}' is too small (minimum ${validation.min})`, path: field.key, expected: `>= ${validation.min}`, actual: value, }); } if (validation.max !== undefined && value > validation.max) { errors.push({ type: "invalid_value", message: `Field '${field.key}' is too large (maximum ${validation.max})`, path: field.key, expected: `<= ${validation.max}`, actual: value, }); } } return errors; } // ======================================== // MANIFEST GENERATION // ======================================== /** * Derive plugin ID from package name * @param packageName Package name (e.g., "@org/my-plugin" or "my-plugin") * @returns Derived plugin ID */ derivePluginId(packageName) { // Remove scope prefix if present (@org/my-plugin -> my-plugin) return packageName.replace(/^@[^/]+\//, ""); } /** * Derive nodeType from package.json tensorify.pluginType * @param pluginType Plugin type from package.json tensorify section * @returns Derived NodeType or default to CUSTOM */ deriveNodeType(pluginType) { if (!pluginType) return core_1.NodeType.CUSTOM; // Map string values to NodeType enum const typeMap = { custom: core_1.NodeType.CUSTOM, trainer: core_1.NodeType.TRAINER, evaluator: core_1.NodeType.EVALUATOR, model: core_1.NodeType.MODEL, model_layer: core_1.NodeType.MODEL_LAYER, dataloader: core_1.NodeType.DATALOADER, preprocessor: core_1.NodeType.PREPROCESSOR, postprocessor: core_1.NodeType.POSTPROCESSOR, augmentation_stack: core_1.NodeType.AUGMENTATION_STACK, optimizer: core_1.NodeType.OPTIMIZER, loss_function: core_1.NodeType.LOSS_FUNCTION, metric: core_1.NodeType.METRIC, scheduler: core_1.NodeType.SCHEDULER, regularizer: core_1.NodeType.REGULARIZER, function: core_1.NodeType.FUNCTION, pipeline: core_1.NodeType.PIPELINE, report: core_1.NodeType.REPORT, }; return typeMap[pluginType.toLowerCase()] || core_1.NodeType.CUSTOM; } /** * Generate frontend manifest for CLI publishing * * @param packageInfo Package.json information * @param entrypointClassName Exact class name for instantiation * @returns Frontend plugin manifest */ generateManifest(packageInfo, entrypointClassName) { // Derive missing core metadata from package.json const derivedId = this.definition.id || this.derivePluginId(packageInfo.name); const derivedName = this.definition.name || packageInfo.name; const derivedDescription = this.definition.description || packageInfo.description || ""; const derivedVersion = this.definition.version || packageInfo.version; const derivedNodeType = this.definition.nodeType || this.deriveNodeType(packageInfo.tensorify?.pluginType); // Validate that we have all required information after derivation if (!derivedId) { throw new Error("Plugin ID could not be derived from package name"); } if (!derivedName) { throw new Error("Plugin name could not be derived from package name"); } if (!derivedVersion) { throw new Error("Plugin version could not be derived from package.json"); } const manifest = { // Package Information name: packageInfo.name, version: packageInfo.version, description: packageInfo.description || derivedDescription, author: packageInfo.author || this.definition.author || "", main: packageInfo.main || "dist/index.js", entrypointClassName, keywords: packageInfo.keywords || this.definition.keywords || [], repository: packageInfo.repository, pluginType: packageInfo.tensorify?.pluginType, tensorify: packageInfo.tensorify, // Frontend Configuration frontendConfigs: { id: derivedId, name: derivedName, category: derivedNodeType, nodeType: derivedNodeType, visual: this.definition.visual, inputHandles: this.definition.inputHandles, outputHandles: this.definition.outputHandles, settingsFields: this.definition.settingsFields, settingsGroups: this.definition.settingsGroups, }, // Plugin Metadata capabilities: this.definition.capabilities, requirements: this.definition.requirements, // Generation Metadata sdkVersion: TensorifyPlugin.SDK_VERSION, generatedAt: new Date().toISOString(), manifestVersion: TensorifyPlugin.MANIFEST_VERSION, }; return manifest; } // ======================================== // UTILITY METHODS // ======================================== /** * Create default settings object from field definitions */ createDefaultSettings() { const settings = { variableName: `${this.definition.id || "plugin"}_${Date.now()}`, labelName: this.definition.name || "Plugin", }; for (const field of this.definition.settingsFields) { if (field.defaultValue !== undefined) { settings[field.key] = field.defaultValue; } } return settings; } /** * Process dynamic label template with settings values */ generateDynamicLabel(settings) { const template = this.definition.visual.labels.dynamicLabelTemplate; if (!template) return ""; let result = template; for (const [key, value] of Object.entries(settings)) { const placeholder = `{${key}}`; result = result.replace(new RegExp(placeholder, "g"), String(value)); } return result; } /** * Get current SDK version */ static getSDKVersion() { return TensorifyPlugin.SDK_VERSION; } /** * Get manifest format version */ static getManifestVersion() { return TensorifyPlugin.MANIFEST_VERSION; } } exports.TensorifyPlugin = TensorifyPlugin; //# sourceMappingURL=TensorifyPlugin.js.map