This commit is contained in:
Josh 2026-01-09 00:24:04 +02:00
commit 91a950e72f
17 changed files with 2724 additions and 0 deletions

497
transform/go.go Normal file
View file

@ -0,0 +1,497 @@
package transform
import (
"fmt"
"regexp"
"strings"
)
// Go type definitions to inject
const goTypeDefinitions = `
// WriteKit SDK Types
type Post struct {
Slug string ` + "`json:\"slug\"`" + `
Title string ` + "`json:\"title\"`" + `
Url string ` + "`json:\"url\"`" + `
Excerpt string ` + "`json:\"excerpt\"`" + `
PublishedAt string ` + "`json:\"publishedAt\"`" + `
UpdatedAt string ` + "`json:\"updatedAt\"`" + `
Tags []string ` + "`json:\"tags\"`" + `
ReadingTime int ` + "`json:\"readingTime\"`" + `
}
type Author struct {
Name string ` + "`json:\"name\"`" + `
Email string ` + "`json:\"email\"`" + `
Avatar string ` + "`json:\"avatar\"`" + `
}
type Blog struct {
Name string ` + "`json:\"name\"`" + `
Url string ` + "`json:\"url\"`" + `
}
type Comment struct {
Id string ` + "`json:\"id\"`" + `
Content string ` + "`json:\"content\"`" + `
AuthorName string ` + "`json:\"authorName\"`" + `
AuthorEmail string ` + "`json:\"authorEmail\"`" + `
PostSlug string ` + "`json:\"postSlug\"`" + `
ParentId string ` + "`json:\"parentId\"`" + `
CreatedAt string ` + "`json:\"createdAt\"`" + `
}
type Member struct {
Email string ` + "`json:\"email\"`" + `
Name string ` + "`json:\"name\"`" + `
SubscribedAt string ` + "`json:\"subscribedAt\"`" + `
}
type Tier struct {
Name string ` + "`json:\"name\"`" + `
Price int ` + "`json:\"price\"`" + `
}
type TitleChange struct {
Old string ` + "`json:\"old\"`" + `
New string ` + "`json:\"new\"`" + `
}
type TagChanges struct {
Added []string ` + "`json:\"added\"`" + `
Removed []string ` + "`json:\"removed\"`" + `
}
type Changes struct {
Title TitleChange ` + "`json:\"title\"`" + `
Content bool ` + "`json:\"content\"`" + `
Tags TagChanges ` + "`json:\"tags\"`" + `
}
type Period struct {
Start string ` + "`json:\"start\"`" + `
End string ` + "`json:\"end\"`" + `
}
type PageView struct {
Path string ` + "`json:\"path\"`" + `
Views int ` + "`json:\"views\"`" + `
}
// Event types
type PostPublishedEvent struct {
Post Post ` + "`json:\"post\"`" + `
Author Author ` + "`json:\"author\"`" + `
Blog Blog ` + "`json:\"blog\"`" + `
}
type PostUpdatedEvent struct {
Post Post ` + "`json:\"post\"`" + `
Author Author ` + "`json:\"author\"`" + `
Changes Changes ` + "`json:\"changes\"`" + `
}
type CommentCreatedEvent struct {
Comment Comment ` + "`json:\"comment\"`" + `
Post Post ` + "`json:\"post\"`" + `
}
type CommentInput struct {
Content string ` + "`json:\"content\"`" + `
AuthorName string ` + "`json:\"authorName\"`" + `
AuthorEmail string ` + "`json:\"authorEmail\"`" + `
PostSlug string ` + "`json:\"postSlug\"`" + `
}
type ValidationResult struct {
Allowed bool ` + "`json:\"allowed\"`" + `
Reason string ` + "`json:\"reason,omitempty\"`" + `
}
type MemberSubscribedEvent struct {
Member Member ` + "`json:\"member\"`" + `
Tier Tier ` + "`json:\"tier\"`" + `
}
type ContentRenderInput struct {
Html string ` + "`json:\"html\"`" + `
Post Post ` + "`json:\"post\"`" + `
}
type ContentRenderOutput struct {
Html string ` + "`json:\"html\"`" + `
}
type AssetUploadedEvent struct {
Id string ` + "`json:\"id\"`" + `
Url string ` + "`json:\"url\"`" + `
ContentType string ` + "`json:\"contentType\"`" + `
Size int ` + "`json:\"size\"`" + `
Width int ` + "`json:\"width\"`" + `
Height int ` + "`json:\"height\"`" + `
}
type AnalyticsSyncEvent struct {
Period Period ` + "`json:\"period\"`" + `
Pageviews int ` + "`json:\"pageviews\"`" + `
Visitors int ` + "`json:\"visitors\"`" + `
TopPages []PageView ` + "`json:\"topPages\"`" + `
}
// Runner SDK wrapper
type runnerSDK struct{}
var Runner = runnerSDK{}
func (r runnerSDK) Log(msg string) {
pdk.Log(pdk.LogInfo, msg)
}
type httpResponse struct {
Status int
Headers map[string]string
Body string
}
func (r runnerSDK) HttpRequest(url, method string, body []byte) httpResponse {
req := pdk.NewHTTPRequest(pdk.MethodPost, url)
if method == "GET" {
req = pdk.NewHTTPRequest(pdk.MethodGet, url)
}
if body != nil {
req.SetBody(body)
}
resp := req.Send()
return httpResponse{
Status: int(resp.Status()),
Body: string(resp.Body()),
}
}
// Secrets accessor (populated at runtime via pdk.GetConfig)
type secretsAccessor struct{}
var Secrets = secretsAccessor{}
`
// Go transforms plugin source code from the clean API to the Extism-compatible format.
//
// Input (clean API):
//
// func OnPostPublished(event PostPublishedEvent) error {
// Runner.Log("Post published: " + event.Post.Title)
// return nil
// }
//
// Output (Extism-compatible):
//
// //export on_post_published
// func onPostPublished() int32 {
// var event PostPublishedEvent
// if err := pdk.InputJSON(&event); err != nil {
// pdk.Log(pdk.LogError, err.Error())
// return 1
// }
// Runner.Log("Post published: " + event.Post.Title)
// return 0
// }
func Go(source string) (string, error) {
result := source
// Transform Runner.Secrets.X to pdk.GetConfig("X") calls
result = transformRunnerSecrets(result)
// Map of hook functions: CleanName -> export_name
hookMap := map[string]string{
"OnPostPublished": "on_post_published",
"OnPostUpdated": "on_post_updated",
"OnCommentCreated": "on_comment_created",
"OnMemberSubscribed": "on_member_subscribed",
"OnAssetUploaded": "on_asset_uploaded",
"OnAnalyticsSync": "on_analytics_sync",
"ValidateComment": "validate_comment",
"RenderContent": "render_content",
}
// Find all hook functions
// Pattern: func FuncName(params) returnType {
funcPattern := regexp.MustCompile(`(?m)^func\s+(\w+)\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]]
// Check if this is a hook function
exportName, isHook := hookMap[funcName]
if !isHook {
continue
}
params := result[match[4]:match[5]]
// Get return type if present
returnType := ""
if match[6] != -1 && match[7] != -1 {
returnType = strings.TrimSpace(result[match[6]:match[7]])
}
// Find the closing brace
braceStart := match[7]
if braceStart == -1 {
braceStart = match[5] + 1
}
// Find the actual opening brace position
for braceStart < len(result) && result[braceStart] != '{' {
braceStart++
}
braceEnd := findClosingBrace(result, braceStart+1)
if braceEnd == -1 {
continue
}
// Extract function body
body := result[braceStart+1 : braceEnd]
// Parse parameters
eventParam, eventType, secretsParam, secretsType := parseGoParams(params)
// Build transformed function
var transformed strings.Builder
// Add export directive
transformed.WriteString(fmt.Sprintf("//export %s\n", exportName))
// New function signature (lowercase name, no params, returns int32)
lowercaseName := strings.ToLower(funcName[:1]) + funcName[1:]
transformed.WriteString(fmt.Sprintf("func %s() int32 {\n", lowercaseName))
// Inject event deserialization if we have an event parameter
if eventParam != "" && eventType != "" {
transformed.WriteString(fmt.Sprintf(" var %s %s\n", eventParam, eventType))
transformed.WriteString(fmt.Sprintf(" if err := pdk.InputJSON(&%s); err != nil {\n", eventParam))
transformed.WriteString(" pdk.Log(pdk.LogError, err.Error())\n")
transformed.WriteString(" return 1\n")
transformed.WriteString(" }\n")
}
// Inject secrets if present
if secretsParam != "" && secretsType != "" {
secretsCode := generateGoSecretsInjection(source, secretsParam, secretsType)
transformed.WriteString(secretsCode)
}
// Transform the body
transformedBody := body
// Check if return type includes a result type (tuple return)
hasResultReturn := strings.Contains(returnType, ",") || (returnType != "" && returnType != "error" && !strings.HasPrefix(returnType, "("))
if hasResultReturn {
// Transform returns that have result values
transformedBody = transformGoReturnsWithResult(body)
} else {
// Transform simple error returns to int32 returns
transformedBody = transformGoReturns(body)
}
transformed.WriteString(transformedBody)
// Ensure we have a return 0 at the end
if !strings.HasSuffix(strings.TrimSpace(transformedBody), "return 0") {
transformed.WriteString("\n return 0\n")
}
transformed.WriteString("}")
// Replace the original function
result = result[:match[0]] + transformed.String() + result[braceEnd+1:]
}
// Inject imports and type definitions
result = injectGoImportsAndTypes(result)
return result, nil
}
// transformRunnerSecrets transforms Runner.Secrets.X to pdk.GetConfig("X") calls
func transformRunnerSecrets(source string) string {
// Transform Runner.Secrets.SomeName to func() string { v, _ := pdk.GetConfig("SOME_NAME"); return v }()
pattern := regexp.MustCompile(`Runner\.Secrets\.(\w+)`)
return pattern.ReplaceAllStringFunc(source, func(match string) string {
m := pattern.FindStringSubmatch(match)
if m != nil {
fieldName := m[1]
configKey := toUpperSnakeCase(fieldName)
return fmt.Sprintf(`func() string { v, _ := pdk.GetConfig("%s"); return v }()`, configKey)
}
return match
})
}
// injectGoImportsAndTypes injects the necessary imports and type definitions
func injectGoImportsAndTypes(source string) string {
// Check if source already has package main
if !strings.Contains(source, "package main") {
source = "package main\n\n" + source
}
// Replace "package main" with package + imports + types
importBlock := `package main
import (
"github.com/extism/go-pdk"
)
` + goTypeDefinitions
// Remove the original package line and add our block
result := regexp.MustCompile(`package\s+main\s*\n*`).ReplaceAllString(source, "")
return importBlock + "\n" + result
}
// parseGoParams extracts parameters from Go function signature
func parseGoParams(params string) (eventParam, eventType, secretsParam, secretsType string) {
params = strings.TrimSpace(params)
if params == "" {
return
}
// Split by comma (handling potential spaces)
parts := splitGoParams(params)
// First parameter is the event
if len(parts) >= 1 {
eventPart := strings.TrimSpace(parts[0])
// Match: paramName ParamType
re := regexp.MustCompile(`(\w+)\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+(\w+)`)
if m := re.FindStringSubmatch(secretsPart); m != nil {
secretsParam = m[1]
secretsType = m[2]
}
}
return
}
// splitGoParams splits Go parameters, handling edge cases
func splitGoParams(params string) []string {
var parts []string
var current strings.Builder
depth := 0
for _, ch := range params {
switch ch {
case '(':
depth++
current.WriteRune(ch)
case ')':
depth--
current.WriteRune(ch)
case ',':
if depth == 0 {
parts = append(parts, current.String())
current.Reset()
} else {
current.WriteRune(ch)
}
default:
current.WriteRune(ch)
}
}
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
// generateGoSecretsInjection creates code to build the secrets struct from pdk.GetConfig() calls
func generateGoSecretsInjection(source, secretsParam, secretsType string) string {
// Find the struct definition for the secrets type
structPattern := regexp.MustCompile(fmt.Sprintf(`(?s)type\s+%s\s+struct\s*\{([^}]*)\}`, regexp.QuoteMeta(secretsType)))
match := structPattern.FindStringSubmatch(source)
if match == nil {
return fmt.Sprintf(" var %s %s\n", secretsParam, secretsType)
}
// Parse struct body for field names
structBody := match[1]
fieldPattern := regexp.MustCompile(`(\w+)\s+string`)
fields := fieldPattern.FindAllStringSubmatch(structBody, -1)
if len(fields) == 0 {
return fmt.Sprintf(" var %s %s\n", secretsParam, secretsType)
}
// Build the secrets struct initialization
var builder strings.Builder
builder.WriteString(fmt.Sprintf(" %s := %s{\n", secretsParam, secretsType))
for _, field := range fields {
fieldName := field[1]
// Convert Go field name to config key (e.g., SlackWebhook -> SLACK_WEBHOOK)
configKey := toUpperSnakeCase(fieldName)
builder.WriteString(fmt.Sprintf(" %s: func() string { v, _ := pdk.GetConfig(\"%s\"); return v }(),\n", fieldName, configKey))
}
builder.WriteString(" }\n")
return builder.String()
}
// toUpperSnakeCase converts CamelCase to UPPER_SNAKE_CASE
func toUpperSnakeCase(s string) string {
var result strings.Builder
for i, ch := range s {
if i > 0 && ch >= 'A' && ch <= 'Z' {
result.WriteRune('_')
}
result.WriteRune(ch)
}
return strings.ToUpper(result.String())
}
// transformGoReturns transforms return statements from error returns to int32 returns
func transformGoReturns(body string) string {
// Replace "return nil" with "return 0"
result := regexp.MustCompile(`return\s+nil\b`).ReplaceAllString(body, "return 0")
// Replace "return err" or "return someErr" with error handling
result = regexp.MustCompile(`return\s+(\w+)\s*$`).ReplaceAllStringFunc(result, func(match string) string {
re := regexp.MustCompile(`return\s+(\w+)`)
m := re.FindStringSubmatch(match)
if m != nil && m[1] != "nil" && m[1] != "0" && m[1] != "1" {
// It's an error variable
return fmt.Sprintf("if %s != nil { pdk.Log(pdk.LogError, %s.Error()); return 1 }; return 0", m[1], m[1])
}
return match
})
return result
}
// transformGoReturnsWithResult transforms returns that have a result value
func transformGoReturnsWithResult(body string) string {
// Match: return result, nil or return result, err
returnPattern := regexp.MustCompile(`return\s+(\w+),\s*(\w+)\s*$`)
return returnPattern.ReplaceAllStringFunc(body, func(match string) string {
m := returnPattern.FindStringSubmatch(match)
if m != nil {
result := m[1]
errVar := m[2]
if errVar == "nil" {
return fmt.Sprintf("pdk.OutputJSON(%s); return 0", result)
}
return fmt.Sprintf("if %s != nil { pdk.Log(pdk.LogError, %s.Error()); return 1 }; pdk.OutputJSON(%s); return 0", errVar, errVar, result)
}
return match
})
}

548
transform/typescript.go Normal file
View file

@ -0,0 +1,548 @@
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<string, string>; 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<Type>(...) -> 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<T> doesn't exist in PDK
return fmt.Sprintf("Host.outputString(JSON.stringify(%s)); return 0;", value)
})
}