549 lines
14 KiB
Go
549 lines
14 KiB
Go
|
|
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)
|
||
|
|
})
|
||
|
|
}
|