package transform import ( "fmt" "regexp" "strings" ) // TypeScript type definitions to inject const tsTypeDefinitions = ` // WriteKit SDK Types interface Post { slug: string; title: string; url: string; excerpt: string; publishedAt: string; updatedAt?: string; tags: string[]; readingTime: number; } interface Author { name: string; email: string; avatar?: string; } interface Blog { name: string; url: string; } interface Comment { id: string; content: string; authorName: string; authorEmail: string; postSlug: string; parentId?: string; createdAt: string; } interface Member { email: string; name?: string; subscribedAt: string; } interface Tier { name: string; price: number; } interface TitleChange { old: string; new: string; } interface TagChanges { added: string[]; removed: string[]; } interface Changes { title?: TitleChange; content?: boolean; tags?: TagChanges; } interface Period { start: string; end: string; } interface PageView { path: string; views: number; } // Event types interface PostPublishedEvent { post: Post; author: Author; blog: Blog; } interface PostUpdatedEvent { post: Post; author: Author; changes: Changes; } interface CommentCreatedEvent { comment: Comment; post: Post; } interface CommentInput { content: string; authorName: string; authorEmail: string; postSlug: string; } interface ValidationResult { allowed: boolean; reason?: string; } interface MemberSubscribedEvent { member: Member; tier: Tier; } interface ContentRenderInput { html: string; post: Post; } interface ContentRenderOutput { html: string; } interface AssetUploadedEvent { id: string; url: string; contentType: string; size: number; width?: number; height?: number; } interface AnalyticsSyncEvent { period: Period; pageviews: number; visitors: number; topPages: PageView[]; } // Runner SDK - simple functions that wrap Host/Config function runnerLog(msg: string): void { Host.log(msg); } function runnerHttpRequest(options: { url: string; method?: string; headers?: Record; body?: string }): any { // For now, just log - full HTTP will be implemented later Host.log("HTTP: " + options.method + " " + options.url); return { status: 200, body: "" }; } function runnerSecretGet(key: string): string { return Config.get(key) || ""; } // Runner namespace object const Runner = { log: runnerLog, httpRequest: runnerHttpRequest, secrets: { get: runnerSecretGet, }, }; ` // TypeScript transforms plugin source code from the clean API to the Extism-compatible format. // extism-js requires CommonJS format (module.exports), not ES6 exports. // // Input (clean API): // // export function onPostPublished(event: PostPublishedEvent, secrets: Secrets): void { // Runner.log(event.post.title); // } // // Output (Extism-compatible CommonJS): // // // Runner SDK wrapper // var Runner = { log: function(msg) { Host.log(msg); }, ... }; // // function onPostPublished() { // var event = JSON.parse(Host.inputString()); // var secrets = { KEY: Config.get("KEY") || "", ... }; // Runner.log(event.post.title); // return 0; // } // module.exports = { onPostPublished }; func TypeScript(source string) (string, error) { result := source var exportedFuncs []string // First, handle arrow functions: export const onPostPublished = (event: PostPublishedEvent): void => { ... } arrowPattern := regexp.MustCompile(`(?s)export\s+const\s+(\w+)\s*=\s*\(([^)]*)\)\s*(?::\s*(\w+))?\s*=>\s*\{`) arrowMatches := arrowPattern.FindAllStringSubmatchIndex(result, -1) // Process arrow function matches in reverse order to preserve indices for i := len(arrowMatches) - 1; i >= 0; i-- { match := arrowMatches[i] funcName := result[match[2]:match[3]] params := result[match[4]:match[5]] // Get return type if present returnType := "void" if match[6] != -1 && match[7] != -1 { returnType = result[match[6]:match[7]] } // Only transform hook functions if !isHookFunction(funcName) { continue } // Track exported functions exportedFuncs = append([]string{funcName}, exportedFuncs...) // Parse parameters eventParam, _, secretsParam, secretsType := parseParams(params) // Find the opening brace after => funcStart := match[0] braceIdx := strings.Index(result[match[0]:], "{") if braceIdx == -1 { continue } braceStart := match[0] + braceIdx + 1 braceEnd := findClosingBrace(result, braceStart) if braceEnd == -1 { continue } // Extract function body body := result[braceStart:braceEnd] // Build the transformed function var transformed strings.Builder snakeFuncName := camelToSnake(funcName) transformed.WriteString(fmt.Sprintf("function %s() {\n", snakeFuncName)) if eventParam != "" { transformed.WriteString(fmt.Sprintf(" var %s = JSON.parse(Host.inputString());\n", eventParam)) } if secretsParam != "" && secretsType != "" { secretsCode := generateSecretsInjectionJS(source, secretsParam, secretsType) transformed.WriteString(secretsCode) } transformedBody := stripTypeAnnotations(body) if returnType != "void" { transformedBody = transformReturnStatements(transformedBody, returnType) } transformed.WriteString(transformedBody) if !strings.Contains(transformedBody, "return 0;") && !strings.Contains(transformedBody, "return 0\n") { transformed.WriteString("\n return 0;\n") } transformed.WriteString("}") result = result[:funcStart] + transformed.String() + result[braceEnd+1:] } // Find all exported hook functions (function declaration syntax) // Match both with and without return type annotation funcPattern := regexp.MustCompile(`(?s)export\s+function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*(\w+))?\s*\{`) matches := funcPattern.FindAllStringSubmatchIndex(result, -1) // Process matches in reverse order to preserve indices for i := len(matches) - 1; i >= 0; i-- { match := matches[i] funcName := result[match[2]:match[3]] params := result[match[4]:match[5]] // Get return type if present (match[6]:match[7] may be -1 if not matched) returnType := "void" if match[6] != -1 && match[7] != -1 { returnType = result[match[6]:match[7]] } // Only transform hook functions if !isHookFunction(funcName) { continue } // Track exported functions exportedFuncs = append([]string{funcName}, exportedFuncs...) // Parse parameters eventParam, _, secretsParam, secretsType := parseParams(params) // Find the matching closing brace for this function funcStart := match[0] // Find the opening brace position bracePos := strings.Index(result[match[5]:], "{") if bracePos == -1 { continue } braceStart := match[5] + bracePos + 1 braceEnd := findClosingBrace(result, braceStart) if braceEnd == -1 { continue // Could not find closing brace } // Extract function body body := result[braceStart:braceEnd] // Build the transformed function (CommonJS style, no type annotations) var transformed strings.Builder // Function declaration without 'export' and without type annotations // Use snake_case name for WASM export (extism-js requires this) snakeFuncName := camelToSnake(funcName) transformed.WriteString(fmt.Sprintf("function %s() {\n", snakeFuncName)) // Inject event deserialization if there was an event parameter if eventParam != "" { transformed.WriteString(fmt.Sprintf(" var %s = JSON.parse(Host.inputString());\n", eventParam)) } // Inject secrets if present if secretsParam != "" && secretsType != "" { secretsCode := generateSecretsInjectionJS(source, secretsParam, secretsType) transformed.WriteString(secretsCode) } // Transform the body: // 1. Remove type annotations // 2. Transform return statements if needed transformedBody := stripTypeAnnotations(body) if returnType != "void" { transformedBody = transformReturnStatements(transformedBody, returnType) } // Add the body transformed.WriteString(transformedBody) // Ensure return 0 at the end if not already present if !strings.Contains(transformedBody, "return 0;") && !strings.Contains(transformedBody, "return 0\n") { transformed.WriteString("\n return 0;\n") } transformed.WriteString("}") // Replace the original function with the transformed one result = result[:funcStart] + transformed.String() + result[braceEnd+1:] } // Remove interface declarations (TypeScript-only) interfacePattern := regexp.MustCompile(`(?s)interface\s+\w+\s*\{[^}]*\}\s*`) result = interfacePattern.ReplaceAllString(result, "") // Remove any remaining 'export' keywords result = regexp.MustCompile(`\bexport\s+`).ReplaceAllString(result, "") // Prepend Runner SDK wrapper runnerSDK := `// Runner SDK wrapper for WriteKit plugins var Runner = { log: function(msg) { Host.log(msg); }, httpRequest: function(options) { var method = options.method || "GET"; var headers = options.headers || {}; var req = { url: options.url, method: method, headers: headers }; var bodyBytes = options.body ? (new TextEncoder()).encode(options.body) : new Uint8Array(0); var resp = Http.request(req, bodyBytes); var respBody = ""; try { respBody = (new TextDecoder()).decode(resp.body()); } catch(e) {} return { status: resp.status, body: respBody, headers: {} }; }, secrets: { get: function(key) { return Config.get(key) || ""; } } }; ` // Add module.exports at the end with snake_case names (extism requires snake_case exports) var exports strings.Builder if len(exportedFuncs) > 0 { exports.WriteString("\nmodule.exports = { ") for i, fn := range exportedFuncs { snakeName := camelToSnake(fn) exports.WriteString(snakeName) if i < len(exportedFuncs)-1 { exports.WriteString(", ") } } exports.WriteString(" };\n") } result = runnerSDK + strings.TrimSpace(result) + exports.String() return result, nil } // stripTypeAnnotations removes TypeScript type annotations from code func stripTypeAnnotations(code string) string { // Remove variable type annotations: const x: Type = ... -> var x = ... code = regexp.MustCompile(`\b(const|let)\s+(\w+)\s*:\s*\w+\s*=`).ReplaceAllString(code, "var $2 =") // Remove 'as Type' casts code = regexp.MustCompile(`\s+as\s+\w+`).ReplaceAllString(code, "") // Remove generic type parameters: func(...) -> func(...) code = regexp.MustCompile(`<\w+>`).ReplaceAllString(code, "") // Replace const/let with var for remaining cases code = regexp.MustCompile(`\b(const|let)\b`).ReplaceAllString(code, "var") return code } // generateSecretsInjectionJS creates JavaScript code to build the secrets object func generateSecretsInjectionJS(source, secretsParam, secretsType string) string { // Find the interface definition for the secrets type interfacePattern := regexp.MustCompile(fmt.Sprintf(`(?s)interface\s+%s\s*\{([^}]*)\}`, regexp.QuoteMeta(secretsType))) match := interfacePattern.FindStringSubmatch(source) if match == nil { // If no interface found, use empty object return fmt.Sprintf(" var %s = {};\n", secretsParam) } // Parse interface body for property names interfaceBody := match[1] propPattern := regexp.MustCompile(`(\w+)\s*:\s*string`) props := propPattern.FindAllStringSubmatch(interfaceBody, -1) if len(props) == 0 { return fmt.Sprintf(" var %s = {};\n", secretsParam) } // Build the secrets object (JavaScript, no types) var builder strings.Builder builder.WriteString(fmt.Sprintf(" var %s = {\n", secretsParam)) for i, prop := range props { key := prop[1] builder.WriteString(fmt.Sprintf(" %s: Config.get(\"%s\") || \"\"", key, key)) if i < len(props)-1 { builder.WriteString(",") } builder.WriteString("\n") } builder.WriteString(" };\n") return builder.String() } // camelToSnake converts camelCase to snake_case // e.g., onPostPublished -> on_post_published func camelToSnake(s string) string { var result strings.Builder for i, r := range s { if i > 0 && r >= 'A' && r <= 'Z' { result.WriteByte('_') } result.WriteRune(r) } return strings.ToLower(result.String()) } // isHookFunction checks if a function name matches a known hook pattern func isHookFunction(name string) bool { hooks := []string{ "onPostPublished", "onPostUpdated", "onCommentCreated", "onMemberSubscribed", "onAssetUploaded", "onAnalyticsSync", "validateComment", "renderContent", } for _, hook := range hooks { if name == hook { return true } } return false } // parseParams extracts event and secrets parameters from the function signature func parseParams(params string) (eventParam, eventType, secretsParam, secretsType string) { params = strings.TrimSpace(params) if params == "" { return } // Split by comma parts := strings.Split(params, ",") // First parameter is the event if len(parts) >= 1 { eventPart := strings.TrimSpace(parts[0]) // Match: paramName: ParamType re := regexp.MustCompile(`(\w+)\s*:\s*(\w+)`) if m := re.FindStringSubmatch(eventPart); m != nil { eventParam = m[1] eventType = m[2] } } // Second parameter (if present) is secrets if len(parts) >= 2 { secretsPart := strings.TrimSpace(parts[1]) re := regexp.MustCompile(`(\w+)\s*:\s*(\w+)`) if m := re.FindStringSubmatch(secretsPart); m != nil { secretsParam = m[1] secretsType = m[2] } } return } // findClosingBrace finds the matching closing brace for an opening brace func findClosingBrace(source string, start int) int { depth := 1 for i := start; i < len(source); i++ { switch source[i] { case '{': depth++ case '}': depth-- if depth == 0 { return i } } } return -1 } // transformReturnStatements wraps return statements with Host.outputString(JSON.stringify()) func transformReturnStatements(body, returnType string) string { // Match return statements with a value // This is a simplified version - a real parser would be more robust returnPattern := regexp.MustCompile(`return\s+(\{[^;]+\}|\w+)\s*;`) return returnPattern.ReplaceAllStringFunc(body, func(match string) string { // Extract the return value re := regexp.MustCompile(`return\s+(.+);`) m := re.FindStringSubmatch(match) if m == nil { return match } value := strings.TrimSpace(m[1]) // Use Host.outputString with JSON.stringify since Host.output doesn't exist in PDK return fmt.Sprintf("Host.outputString(JSON.stringify(%s)); return 0;", value) }) }