From e41931f30a839c66097024e705c4c27ed5374491 Mon Sep 17 00:00:00 2001 From: Jimmy Moore <67790371+jimmyaxod@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:49:14 +0000 Subject: [PATCH] Jm/ext ts host (#117) Scale-65 This adds typescript host support for extensions. Scale-65 --------- Signed-off-by: Jimmy Moore Signed-off-by: Shivansh Vij Co-authored-by: Shivansh Vij --- config.ts | 7 + extension/generator/generator.go | 81 ++- extension/generator/typescript/generated.txt | 219 +++++++ extension/generator/typescript/generator.go | 547 ++++++++++++++++++ .../generator/typescript/generator_test.go | 63 ++ extension/generator/typescript/guest.txt | 5 + extension/generator/typescript/host.txt | 110 ++++ .../generator/typescript/packagejson.txt | 20 + .../templates/declaration-guest.ts.templ | 4 + .../templates/declaration-host.ts.templ | 32 + .../typescript/templates/guest.ts.templ | 5 + .../typescript/templates/header.ts.templ | 2 + .../typescript/templates/host.ts.templ | 165 ++++++ .../typescript/templates/package.ts.templ | 20 + .../typescript/templates/templates.go | 19 + integration/generate_ext_test.go | 14 +- integration/integration.ext.test.ts | 83 +++ integration/integration_ext_test.go | 43 +- .../host_extension/index.d.ts | 23 + .../host_extension/index.js | 108 ++++ .../host_extension/index.js.map | 8 + .../host_extension/index.ts | 125 ++++ .../host_extension/package.json | 20 + .../host_extension/types.d.ts | 29 + .../host_extension/types.js | 68 +++ .../host_extension/types.js.map | 8 + .../host_extension/types.ts | 51 ++ module.ts | 56 +- package-lock.json | 6 + package.json | 1 + storage/extension.go | 17 + 31 files changed, 1947 insertions(+), 12 deletions(-) create mode 100755 extension/generator/typescript/generated.txt create mode 100644 extension/generator/typescript/generator.go create mode 100644 extension/generator/typescript/generator_test.go create mode 100644 extension/generator/typescript/guest.txt create mode 100644 extension/generator/typescript/host.txt create mode 100644 extension/generator/typescript/packagejson.txt create mode 100644 extension/generator/typescript/templates/declaration-guest.ts.templ create mode 100644 extension/generator/typescript/templates/declaration-host.ts.templ create mode 100644 extension/generator/typescript/templates/guest.ts.templ create mode 100644 extension/generator/typescript/templates/header.ts.templ create mode 100644 extension/generator/typescript/templates/host.ts.templ create mode 100644 extension/generator/typescript/templates/package.ts.templ create mode 100644 extension/generator/typescript/templates/templates.go create mode 100644 integration/integration.ext.test.ts create mode 100644 integration/typescript_ext_tests/host_extension/index.d.ts create mode 100644 integration/typescript_ext_tests/host_extension/index.js create mode 100644 integration/typescript_ext_tests/host_extension/index.js.map create mode 100644 integration/typescript_ext_tests/host_extension/index.ts create mode 100644 integration/typescript_ext_tests/host_extension/package.json create mode 100644 integration/typescript_ext_tests/host_extension/types.d.ts create mode 100644 integration/typescript_ext_tests/host_extension/types.js create mode 100644 integration/typescript_ext_tests/host_extension/types.js.map create mode 100644 integration/typescript_ext_tests/host_extension/types.ts diff --git a/config.ts b/config.ts index 71ff912d..d50a4f88 100644 --- a/config.ts +++ b/config.ts @@ -16,6 +16,7 @@ import { Signature, New } from "@loopholelabs/scale-signature-interfaces"; import { V1BetaSchema } from "./scalefunc/scalefunc"; +import { Extension } from "@loopholelabs/scale-extension-interfaces"; const envStringRegex = /[^A-Za-z0-9_]/; @@ -38,6 +39,7 @@ export class Config { stdout: Writer | undefined; stderr: Writer | undefined; rawOutput: boolean = false; + public extensions: Extension[] = []; constructor(newSignature: New) { this.newSignature = newSignature; @@ -87,6 +89,11 @@ export class Config { return this; } + public WithExtension(e: Extension): Config { + this.extensions.push(e); + return this; + } + public WithStdout(writer: Writer): Config { this.stdout = writer return this; diff --git a/extension/generator/generator.go b/extension/generator/generator.go index 2d62b741..6b09395c 100644 --- a/extension/generator/generator.go +++ b/extension/generator/generator.go @@ -14,12 +14,17 @@ package generator import ( + "archive/tar" "bytes" + "compress/gzip" "encoding/hex" + "fmt" + "path" "github.com/loopholelabs/scale/extension" "github.com/loopholelabs/scale/extension/generator/golang" "github.com/loopholelabs/scale/extension/generator/rust" + "github.com/loopholelabs/scale/extension/generator/typescript" ) type GuestRegistryPackage struct { @@ -45,8 +50,9 @@ type HostRegistryPackage struct { } type HostLocalPackage struct { - GolangFiles []File - TypescriptFiles []File + GolangFiles []File + TypescriptFiles []File + TypescriptPackage *bytes.Buffer } type Options struct { @@ -157,7 +163,76 @@ func GenerateHostLocal(options *Options) (*HostLocalPackage, error) { NewFile("go.mod", "go.mod", modfile), } + typescriptTypes, err := typescript.GenerateTypesTranspiled(options.Extension, options.TypescriptPackageName, "types.js") + if err != nil { + return nil, err + } + + typescriptHost, err := typescript.GenerateHostTranspiled(options.Extension, hashString, options.TypescriptPackageName, "index.js") + if err != nil { + return nil, err + } + + packageJSON, err := typescript.GeneratePackageJSON(options.TypescriptPackageName, options.TypescriptPackageVersion) + if err != nil { + return nil, err + } + + typescriptFiles := []File{ + NewFile("types.ts", "types.ts", typescriptTypes.Typescript), + NewFile("types.js", "types.js", typescriptTypes.Javascript), + NewFile("types.js.map", "types.js.map", typescriptTypes.SourceMap), + NewFile("types.d.ts", "types.d.ts", typescriptTypes.Declaration), + NewFile("index.ts", "index.ts", typescriptHost.Typescript), + NewFile("index.js", "index.js", typescriptHost.Javascript), + NewFile("index.js.map", "index.js.map", typescriptHost.SourceMap), + NewFile("index.d.ts", "index.d.ts", typescriptHost.Declaration), + NewFile("package.json", "package.json", packageJSON), + } + + typescriptBuffer := new(bytes.Buffer) + gzipTypescriptWriter := gzip.NewWriter(typescriptBuffer) + tarTypescriptWriter := tar.NewWriter(gzipTypescriptWriter) + + var header *tar.Header + for _, file := range typescriptFiles { + header, err = tar.FileInfoHeader(file, file.Name()) + if err != nil { + _ = tarTypescriptWriter.Close() + _ = gzipTypescriptWriter.Close() + return nil, fmt.Errorf("failed to create tar header for %s: %w", file.Name(), err) + } + + header.Name = path.Join("package", header.Name) + + err = tarTypescriptWriter.WriteHeader(header) + if err != nil { + _ = tarTypescriptWriter.Close() + _ = gzipTypescriptWriter.Close() + return nil, fmt.Errorf("failed to write tar header for %s: %w", file.Name(), err) + } + _, err = tarTypescriptWriter.Write(file.Data()) + if err != nil { + _ = tarTypescriptWriter.Close() + _ = gzipTypescriptWriter.Close() + return nil, fmt.Errorf("failed to write tar data for %s: %w", file.Name(), err) + } + } + + err = tarTypescriptWriter.Close() + if err != nil { + return nil, fmt.Errorf("failed to close tar writer: %w", err) + } + + err = gzipTypescriptWriter.Close() + if err != nil { + return nil, fmt.Errorf("failed to close gzip writer: %w", err) + } + return &HostLocalPackage{ - GolangFiles: golangFiles, + GolangFiles: golangFiles, + TypescriptFiles: typescriptFiles, + TypescriptPackage: typescriptBuffer, }, nil + } diff --git a/extension/generator/typescript/generated.txt b/extension/generator/typescript/generated.txt new file mode 100755 index 00000000..5b75466b --- /dev/null +++ b/extension/generator/typescript/generated.txt @@ -0,0 +1,219 @@ +// Code generated by scale-signature 0.4.5, DO NOT EDIT. +// output: types + +import { Encoder, Decoder, Kind } from "@loopholelabs/polyglot" + +export class HttpConfig { + timeout: number; + + /** + * @throws {Error} + */ + constructor (decoder?: Decoder) { + if (decoder) { + let err: Error | undefined; + try { + err = decoder.error(); + } catch (_) {} + if (typeof err !== "undefined") { + throw err; + } + this.timeout = decoder.int32(); + } else { + this.timeout = 60; + } + } + + /** + * @throws {Error} + */ + encode (encoder: Encoder) { + encoder.int32(this.timeout); + } + + /** + * @throws {Error} + */ + static decode (decoder: Decoder): HttpConfig | undefined { + if (decoder.null()) { + return undefined + } + return new HttpConfig(decoder); + } + + /** + * @throws {Error} + */ + static encode_undefined (encoder: Encoder) { + encoder.null(); + } +} + +export class HttpResponse { + headers: Map; + + statusCode: number; + + body: Uint8Array; + + /** + * @throws {Error} + */ + constructor (decoder?: Decoder) { + if (decoder) { + let err: Error | undefined; + try { + err = decoder.error(); + } catch (_) {} + if (typeof err !== "undefined") { + throw err; + } + this.headers = new Map(); + let headersSize = decoder.map(Kind.String, Kind.Any); + for (let i = 0; i < headersSize; i++) { + let key = decoder.string(); + let val = StringList.decode(decoder); + if (typeof val !== "undefined") { + this.headers.set(key, val); + } + } + this.statusCode = decoder.int32(); + this.body = decoder.uint8Array(); + } else { + this.headers = new Map(); + this.statusCode = 0; + this.body = new Uint8Array(0); + } + } + + /** + * @throws {Error} + */ + encode (encoder: Encoder) { + encoder.map(this.headers.size, Kind.String, Kind.Any); + this.headers.forEach((val, key) => { + encoder.string(key); + val.encode(encoder); + }); + encoder.int32(this.statusCode); + encoder.uint8Array(this.body); + } + + /** + * @throws {Error} + */ + static decode (decoder: Decoder): HttpResponse | undefined { + if (decoder.null()) { + return undefined + } + return new HttpResponse(decoder); + } + + /** + * @throws {Error} + */ + static encode_undefined (encoder: Encoder) { + encoder.null(); + } +} + +export class StringList { + values: string[]; + + /** + * @throws {Error} + */ + constructor (decoder?: Decoder) { + if (decoder) { + let err: Error | undefined; + try { + err = decoder.error(); + } catch (_) {} + if (typeof err !== "undefined") { + throw err; + } + const valuesSize = decoder.array(Kind.String); + this.values = new Array(valuesSize); + for (let i = 0; i < valuesSize; i += 1) { + this.values[i] = decoder.string(); + } + } else { + this.values = []; + } + } + + /** + * @throws {Error} + */ + encode (encoder: Encoder) { + const valuesLength = this.values.length; + encoder.array(valuesLength, Kind.String); + for (let i = 0; i < valuesLength; i += 1) { + encoder.string(this.values[i]); + } + } + + /** + * @throws {Error} + */ + static decode (decoder: Decoder): StringList | undefined { + if (decoder.null()) { + return undefined + } + return new StringList(decoder); + } + + /** + * @throws {Error} + */ + static encode_undefined (encoder: Encoder) { + encoder.null(); + } +} + +export class ConnectionDetails { + url: string; + + /** + * @throws {Error} + */ + constructor (decoder?: Decoder) { + if (decoder) { + let err: Error | undefined; + try { + err = decoder.error(); + } catch (_) {} + if (typeof err !== "undefined") { + throw err; + } + this.url = decoder.string(); + } else { + this.url = "https://google.com"; + } + } + + /** + * @throws {Error} + */ + encode (encoder: Encoder) { + encoder.string(this.url); + } + + /** + * @throws {Error} + */ + static decode (decoder: Decoder): ConnectionDetails | undefined { + if (decoder.null()) { + return undefined + } + return new ConnectionDetails(decoder); + } + + /** + * @throws {Error} + */ + static encode_undefined (encoder: Encoder) { + encoder.null(); + } +} + diff --git a/extension/generator/typescript/generator.go b/extension/generator/typescript/generator.go new file mode 100644 index 00000000..e334ae21 --- /dev/null +++ b/extension/generator/typescript/generator.go @@ -0,0 +1,547 @@ +/* + Copyright 2023 Loophole Labs + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package typescript + +import ( + "bytes" + "errors" + "fmt" + "strings" + "text/template" + + "github.com/evanw/esbuild/pkg/api" + polyglotVersion "github.com/loopholelabs/polyglot/version" + + interfacesVersion "github.com/loopholelabs/scale-extension-interfaces/version" + "github.com/loopholelabs/scale/signature" + scaleVersion "github.com/loopholelabs/scale/version" + + "github.com/loopholelabs/scale/extension" + "github.com/loopholelabs/scale/extension/generator/typescript/templates" + "github.com/loopholelabs/scale/signature/generator/typescript" + + "github.com/loopholelabs/scale/signature/generator/utils" +) + +const ( + defaultPackageName = "types" + tsConfig = ` +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "sourceMap": true, + "paths": { + "signature": ["./"] + }, + "types": ["node"] + }, +}` +) + +var generator *Generator + +type Transpiled struct { + Typescript []byte + Javascript []byte + SourceMap []byte + Declaration []byte +} + +// GenerateTypes generates the types for the extension +func GenerateTypes(extensionSchema *extension.Schema, packageName string) ([]byte, error) { + return generator.GenerateTypes(extensionSchema, packageName) +} + +// GenerateTypesTranspiled generates the types for the extension and transpiles it to javascript +func GenerateTypesTranspiled(extensionSchema *extension.Schema, packageName string, sourceName string) (*Transpiled, error) { + typescriptSource, err := generator.GenerateTypes(extensionSchema, packageName) + if err != nil { + return nil, err + } + return generator.GenerateTypesTranspiled(extensionSchema, packageName, sourceName, string(typescriptSource)) +} + +// GeneratePackageJSON generates the package.json file for the extension +func GeneratePackageJSON(packageName string, packageVersion string) ([]byte, error) { + return generator.GeneratePackageJSON(packageName, packageVersion) +} + +// GenerateGuest generates the guest bindings for the extension +func GenerateGuest(extensionSchema *extension.Schema, extensionHash string, packageName string) ([]byte, error) { + return generator.GenerateGuest(extensionSchema, extensionHash, packageName) +} + +// GenerateGuestTranspiled generates the guest bindings and transpiles it to javascript +func GenerateGuestTranspiled(extensionSchema *extension.Schema, extensionHash string, packageName string, sourceName string) (*Transpiled, error) { + typescriptSource, err := generator.GenerateGuest(extensionSchema, extensionHash, packageName) + if err != nil { + return nil, err + } + return generator.GenerateGuestTranspiled(extensionSchema, packageName, sourceName, string(typescriptSource)) +} + +// GenerateHost generates the host bindings for the extension +// +// Note: the given schema should already be normalized, validated, and modified to have its accessors and validators disabled +func GenerateHost(extensionSchema *extension.Schema, extensionHash string, packageName string) ([]byte, error) { + return generator.GenerateHost(extensionSchema, extensionHash, packageName) +} + +// GenerateHostTranspiled generates the host bindings and transpiles it to javascript +// +// Note: the given schema should already be normalized, validated, and modified to have its accessors and validators disabled +func GenerateHostTranspiled(extensionSchema *extension.Schema, extensionHash string, packageName string, sourceName string) (*Transpiled, error) { + typescriptSource, err := generator.GenerateHost(extensionSchema, extensionHash, packageName) + + if err != nil { + return nil, err + } + return generator.GenerateHostTranspiled(extensionSchema, packageName, sourceName, string(typescriptSource)) +} + +func init() { + var err error + generator, err = New() + if err != nil { + panic(err) + } +} + +// Generator is the typescript generator +type Generator struct { + templ *template.Template + signature *typescript.Generator +} + +// New creates a new typescript generator +func New() (*Generator, error) { + templ, err := template.New("").Funcs(templateFunctions()).ParseFS(templates.FS, "*.ts.templ") + if err != nil { + return nil, err + } + + sig, err := typescript.New() + if err != nil { + return nil, err + } + + return &Generator{ + templ: templ, + signature: sig, + }, nil +} + +// GenerateTypes generates the types for the extension +// +// This is not transpiled to javascript and does not include source maps or type definitions +func (g *Generator) GenerateTypes(extensionSchema *extension.Schema, packageName string) ([]byte, error) { + signatureSchema := &signature.Schema{ + Version: extensionSchema.Version, + Enums: extensionSchema.Enums, + Models: extensionSchema.Models, + } + + signatureSchema.SetHasLengthValidator(extensionSchema.HasLengthValidator()) + signatureSchema.SetHasCaseModifier(extensionSchema.HasCaseModifier()) + signatureSchema.SetHasLimitValidator(extensionSchema.HasLimitValidator()) + signatureSchema.SetHasRegexValidator(extensionSchema.HasRegexValidator()) + + s, err := g.signature.GenerateTypes(signatureSchema, packageName) + + return s, err +} + +// GenerateTypesTranspiled takes the typescript source for the generated types and transpiles it to javascript +func (g *Generator) GenerateTypesTranspiled(extensionSchema *extension.Schema, packageName string, sourceName string, typescriptSource string) (*Transpiled, error) { + signatureSchema := &signature.Schema{ + Version: extensionSchema.Version, + Enums: extensionSchema.Enums, + Models: extensionSchema.Models, + } + + signatureSchema.SetHasLengthValidator(extensionSchema.HasLengthValidator()) + signatureSchema.SetHasCaseModifier(extensionSchema.HasCaseModifier()) + signatureSchema.SetHasLimitValidator(extensionSchema.HasLimitValidator()) + signatureSchema.SetHasRegexValidator(extensionSchema.HasRegexValidator()) + + st, err := g.signature.GenerateTypesTranspiled(signatureSchema, packageName, sourceName, typescriptSource) + if err != nil { + return nil, err + } + + return &Transpiled{ + Typescript: st.Typescript, + Javascript: st.Javascript, + SourceMap: st.SourceMap, + Declaration: st.Declaration, + }, nil +} + +// GeneratePackageJSON generates the package.json file for the extension +func (g *Generator) GeneratePackageJSON(packageName string, packageVersion string) ([]byte, error) { + buf := new(bytes.Buffer) + err := g.templ.ExecuteTemplate(buf, "package.ts.templ", map[string]any{ + "polyglot_version": strings.TrimPrefix(polyglotVersion.Version(), "v"), + "scale_extension_interfaces_version": strings.TrimPrefix(interfacesVersion.Version(), "v"), + "package_name": packageName, + "package_version": strings.TrimPrefix(packageVersion, "v"), + }) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// GenerateGuest generates the guest bindings for the extension +func (g *Generator) GenerateGuest(extensionSchema *extension.Schema, extensionHash string, packageName string) ([]byte, error) { + if packageName == "" { + packageName = defaultPackageName + } + + buf := new(bytes.Buffer) + err := g.templ.ExecuteTemplate(buf, "guest.ts.templ", map[string]any{ + "extension_schema": extensionSchema, + "extension_hash": extensionHash, + "generator_version": strings.TrimPrefix(scaleVersion.Version(), "v"), + "package_name": packageName, + }) + if err != nil { + return nil, err + } + + return []byte(formatTS(buf.String())), nil +} + +// GenerateGuestTranspiled takes the typescript source for the generated guest bindings and transpiles it to javascript +func (g *Generator) GenerateGuestTranspiled(extensionSchema *extension.Schema, packageName string, sourceName string, typescriptSource string) (*Transpiled, error) { + result := api.Transform(typescriptSource, api.TransformOptions{ + Loader: api.LoaderTS, + Format: api.FormatCommonJS, + Sourcemap: api.SourceMapExternal, + SourceRoot: sourceName, + TsconfigRaw: tsConfig, + }) + + if len(result.Errors) > 0 { + var errString strings.Builder + for _, err := range result.Errors { + errString.WriteString(err.Text) + errString.WriteRune('\n') + } + return nil, errors.New(errString.String()) + } + if packageName == "" { + packageName = defaultPackageName + } + + headerBuf := new(bytes.Buffer) + err := g.templ.ExecuteTemplate(headerBuf, "header.ts.templ", map[string]any{ + "generator_version": strings.Trim(scaleVersion.Version(), "v"), + "package_name": packageName, + }) + if err != nil { + return nil, err + } + + declarationBuf := new(bytes.Buffer) + err = g.templ.ExecuteTemplate(declarationBuf, "declaration-guest.ts.templ", map[string]any{ + "extension_schema": extensionSchema, + "generator_version": strings.TrimPrefix(scaleVersion.Version(), "v"), + "package_name": packageName, + }) + if err != nil { + return nil, err + } + + return &Transpiled{ + Typescript: []byte(typescriptSource), + Javascript: append(append([]byte(headerBuf.String()+"\n\n"), result.Code...), []byte(fmt.Sprintf("//# sourceMappingURL=%s.map", sourceName))...), + SourceMap: result.Map, + Declaration: []byte(formatTS(declarationBuf.String())), + }, nil +} + +// GenerateHost generates the host bindings for the extension +// +// Note: the given schema should already be normalized, validated, and modified to have its accessors and validators disabled +func (g *Generator) GenerateHost(extensionSchema *extension.Schema, extensionHash string, packageName string) ([]byte, error) { + schema, err := extensionSchema.CloneWithDisabledAccessorsValidatorsAndModifiers() + if err != nil { + return nil, err + } + + if packageName == "" { + packageName = defaultPackageName + } + + buf := new(bytes.Buffer) + err = g.templ.ExecuteTemplate(buf, "host.ts.templ", map[string]any{ + "extension_schema": schema, + "extension_hash": extensionHash, + "generator_version": strings.TrimPrefix(scaleVersion.Version(), "v"), + "package_name": packageName, + }) + if err != nil { + return nil, err + } + + return []byte(formatTS(buf.String())), nil +} + +// GenerateHostTranspiled takes the typescript source for the generated host bindings and transpiles it to javascript +// +// Note: the given schema should already be normalized, validated, and modified to have its accessors and validators disabled +func (g *Generator) GenerateHostTranspiled(extensionSchema *extension.Schema, packageName string, sourceName string, typescriptSource string) (*Transpiled, error) { + schema, err := extensionSchema.CloneWithDisabledAccessorsValidatorsAndModifiers() + if err != nil { + return nil, err + } + + result := api.Transform(typescriptSource, api.TransformOptions{ + Loader: api.LoaderTS, + Format: api.FormatCommonJS, + Sourcemap: api.SourceMapExternal, + SourceRoot: sourceName, + TsconfigRaw: tsConfig, + }) + + if len(result.Errors) > 0 { + var errString strings.Builder + for _, err := range result.Errors { + errString.WriteString(err.Text) + errString.WriteRune('\n') + } + return nil, errors.New(errString.String()) + } + if packageName == "" { + packageName = defaultPackageName + } + + headerBuf := new(bytes.Buffer) + err = g.templ.ExecuteTemplate(headerBuf, "header.ts.templ", map[string]any{ + "generator_version": strings.Trim(scaleVersion.Version(), "v"), + "package_name": packageName, + }) + if err != nil { + return nil, err + } + + declarationBuf := new(bytes.Buffer) + err = g.templ.ExecuteTemplate(declarationBuf, "declaration-host.ts.templ", map[string]any{ + "extension_schema": schema, + "generator_version": strings.TrimPrefix(scaleVersion.Version(), "v"), + "package_name": packageName, + }) + if err != nil { + return nil, err + } + + return &Transpiled{ + Typescript: []byte(typescriptSource), + Javascript: append(append([]byte(headerBuf.String()+"\n\n"), result.Code...), []byte(fmt.Sprintf("//# sourceMappingURL=%s.map", sourceName))...), + SourceMap: result.Map, + Declaration: []byte(formatTS(declarationBuf.String())), + }, nil +} + +func templateFunctions() template.FuncMap { + return template.FuncMap{ + "IsInterface": isInterface, + "Primitive": primitive, + "IsPrimitive": extension.ValidPrimitiveType, + "PolyglotPrimitive": polyglotPrimitive, + "PolyglotPrimitiveEncode": polyglotPrimitiveEncode, + "PolyglotPrimitiveDecode": polyglotPrimitiveDecode, + "Deref": func(i *bool) bool { return *i }, + "CamelCase": utils.CamelCase, + "Params": utils.Params, + "Constructor": constructor, + } +} + +func isInterface(schema *extension.Schema, s string) bool { + for _, i := range schema.Interfaces { + if i.Name == s { + return true + } + } + return false +} + +func primitive(t string) string { + switch t { + case "string": + return "string" + case "int32": + return "number" + case "int64": + return "bigint" + case "uint32": + return "number" + case "uint64": + return "bigint" + case "float32": + return "number" + case "float64": + return "number" + case "bool": + return "boolean" + case "bytes": + return "Uint8Array" + default: + return t + } +} + +func constructor(t string) string { + switch t { + case "string": + return "String" + case "int32": + return "Number" + case "int64": + return "BigInt" + case "uint32": + return "Number" + case "uint64": + return "BigInt" + case "float32": + return "Number" + case "float64": + return "Number" + case "bool": + return "Boolean" + case "bytes": + return "Uint8Array" + default: + return t + } +} + +func polyglotPrimitive(t string) string { + switch t { + case "string": + return "Kind.String" + case "int32": + return "Kind.Int32" + case "int64": + return "Kind.Int64" + case "uint32": + return "Kind.Uint32" + case "uint64": + return "Kind.Uint64" + case "float32": + return "Kind.Float32" + case "float64": + return "Kind.Float64" + case "bool": + return "Kind.Boolean" + case "bytes": + return "Kind.Uint8Array" + default: + return "Kind.Any" + } +} + +func polyglotPrimitiveEncode(t string) string { + switch t { + case "string": + return "string" + case "int32": + return "int32" + case "int64": + return "int64" + case "uint32": + return "uint32" + case "uint64": + return "uint64" + case "float32": + return "float32" + case "float64": + return "float64" + case "bool": + return "boolean" + case "bytes": + return "uint8Array" + default: + return t + } +} + +func polyglotPrimitiveDecode(t string) string { + switch t { + case "string": + return "string" + case "int32": + return "int32" + case "int64": + return "int64" + case "uint32": + return "uint32" + case "uint64": + return "uint64" + case "float32": + return "float32" + case "float64": + return "float64" + case "bool": + return "boolean" + case "bytes": + return "uint8Array" + default: + return "" + } +} + +//nolint:revive +func formatTS(code string) string { + var output strings.Builder + indentLevel := 0 + lastLineEmpty := false + lastLineOpenBrace := false + for _, line := range strings.Split(code, "\n") { + trimmedLine := strings.TrimSpace(line) + if trimmedLine == "" { + // Allow empty lines between classes and class members, but only 1 empty line not more. + if indentLevel > 1 || lastLineEmpty || lastLineOpenBrace { + continue + } else { + output.WriteRune('\n') + } + lastLineEmpty = true + } else { + if strings.HasPrefix(trimmedLine, "}") { + indentLevel-- + } + output.WriteString(strings.Repeat(" ", indentLevel)) + output.WriteString(trimmedLine) + if strings.HasSuffix(trimmedLine, "{") { + lastLineOpenBrace = true + indentLevel++ + } else { + lastLineOpenBrace = false + } + output.WriteRune('\n') + lastLineEmpty = false + } + } + return output.String() +} diff --git a/extension/generator/typescript/generator_test.go b/extension/generator/typescript/generator_test.go new file mode 100644 index 00000000..6fdd26f2 --- /dev/null +++ b/extension/generator/typescript/generator_test.go @@ -0,0 +1,63 @@ +//go:build !integration && !generate + +/* + Copyright 2023 Loophole Labs + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package typescript + +import ( + "os" + "testing" + + "github.com/loopholelabs/scale/extension" + + "github.com/stretchr/testify/require" +) + +func TestGenerator(t *testing.T) { + s := new(extension.Schema) + err := s.Decode([]byte(extension.MasterTestingSchema)) + require.NoError(t, err) + + packageName := "fetch" + + formatted, err := GenerateTypes(s, "types") + require.NoError(t, err) + // os.WriteFile("./generated.txt", formatted, 0644) + + expTypes, err := os.ReadFile("./generated.txt") + require.NoError(t, err) + require.Equal(t, string(expTypes), string(formatted)) + + host, err := GenerateHost(s, packageName, "v0.1.0") + require.NoError(t, err) + // os.WriteFile("./host.txt", host, 0644) + expHost, err := os.ReadFile("./host.txt") + require.NoError(t, err) + require.Equal(t, string(expHost), string(host)) + + guest, err := GenerateGuest(s, packageName, "v0.1.0") + require.NoError(t, err) + // os.WriteFile("./guest.txt", guest, 0644) + expGuest, err := os.ReadFile("./guest.txt") + require.NoError(t, err) + require.Equal(t, string(expGuest), string(guest)) + + mod, err := GeneratePackageJSON(packageName, "v0.1.0") + require.NoError(t, err) + // os.WriteFile("./packagejson.txt", mod, 0644) + expMod, err := os.ReadFile("./packagejson.txt") + require.NoError(t, err) + require.Equal(t, string(expMod), string(mod)) + +} diff --git a/extension/generator/typescript/guest.txt b/extension/generator/typescript/guest.txt new file mode 100644 index 00000000..3ffcfa2e --- /dev/null +++ b/extension/generator/typescript/guest.txt @@ -0,0 +1,5 @@ +// Code generated by scale-extension 0.4.5, DO NOT EDIT. +// output: v0.1.0 + +/* eslint no-bitwise: off */ + diff --git a/extension/generator/typescript/host.txt b/extension/generator/typescript/host.txt new file mode 100644 index 00000000..348a845e --- /dev/null +++ b/extension/generator/typescript/host.txt @@ -0,0 +1,110 @@ +// Code generated by scale-extension 0.4.5, DO NOT EDIT. +// output: v0.1.0 + +/* eslint no-bitwise: off */ + +import { Extension as ExtensionInterface, ModuleMemory, Resizer } from "@loopholelabs/scale-extension-interfaces"; +import { Decoder, Encoder, Kind } from "@loopholelabs/polyglot"; +import * as types from "./types"; + +export * from "./types"; + +const hash = "fetch"; + +// Write an error to the scale function guest buffer. +function hostError(mem: ModuleMemory, resize: Resizer, err: Error) { + const enc = new Encoder(); + enc.error(err); + const ptr = resize("ext_fetch_Resize", enc.bytes.length); + + mem.Write(ptr, enc.bytes); +} + +class hostExt { + functions: Map; + host: Host; + + constructor(fns: Map, h: Host) { + this.functions = fns; + this.host = h; + } + + Init(): Map { + return this.functions; + } + + Reset() { + // Reset any instances that have been created. + this.host.instances_HttpConnector = new Map(); + + // Add global functions to the runtime + + fns.set("ext_fetch_New", hostWrapper.host_ext_fetch_New.bind(hostWrapper)); + + hostWrapper.instances_HttpConnector = new Map(); + + fns.set("ext_fetch_HttpConnector_Fetch", hostWrapper.host_ext_fetch_HttpConnector_Fetch.bind(hostWrapper)); + + return new hostExt(fns, hostWrapper); +} + +class Host { + impl: Interface + + gid_HttpConnector: bigint = 0n; + instances_HttpConnector: Map = new Map(); + + constructor(i: Interface) { + this.impl = i; + } + + // Global functions... + + host_ext_fetch_New(mem: ModuleMemory, resize: Resizer, params: number[]) { + const d = mem.Read(params[1], params[2]); + const c = types.HttpConfig.decode(new Decoder(d)); + const r = this.impl.New(c); + const id = this.gid_HttpConnector++; + this.instances_HttpConnector.set(id, r); + params[0] = id; + return; + } + + // Instance functions... + + host_ext_fetch_HttpConnector_Fetch(mem: ModuleMemory, resize: Resizer, params: number[]): bigint { + const d = mem.Read(params[1], params[2]); + const c = types.ConnectionDetails.decode(new Decoder(d)); + // Do lookup... + const inst = this.instances_HttpConnector.get(params[0]); + const r = inst.Fetch(c); + const enc = new Encoder(); + r.encode(enc); + const ptr = resize("ext_fetch_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + } + +} + +//// //// //// //// //// //// //// //// //// + +// Interface to the extension impl. This is what the implementor should create + +export interface Interface { + New(params: HttpConfig): HttpConnector; + +} + +export interface HttpConnector { + Fetch(params: ConnectionDetails): HttpResponse; + +} + diff --git a/extension/generator/typescript/packagejson.txt b/extension/generator/typescript/packagejson.txt new file mode 100644 index 00000000..ad7195c4 --- /dev/null +++ b/extension/generator/typescript/packagejson.txt @@ -0,0 +1,20 @@ +{ + "name": "fetch", + "version": "0.1.0", + "source": "index.ts", + "types": "index.d.ts", + "scripts": { + "test": "jest index.test.ts" + }, + "devDependencies": { + "@types/jest": "^29.5.2", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "typescript": "^5.1.3", + "@types/node": "^20.3.1" + }, + "dependencies": { + "@loopholelabs/polyglot": "^1.1.3", + "@loopholelabs/scale-extension-interfaces": "^0.1.0" + } +} diff --git a/extension/generator/typescript/templates/declaration-guest.ts.templ b/extension/generator/typescript/templates/declaration-guest.ts.templ new file mode 100644 index 00000000..b7898a46 --- /dev/null +++ b/extension/generator/typescript/templates/declaration-guest.ts.templ @@ -0,0 +1,4 @@ +// Code generated by scale-extension {{ .generator_version }}, DO NOT EDIT. +// output: {{ .package_name }} + +export * from "./types"; diff --git a/extension/generator/typescript/templates/declaration-host.ts.templ b/extension/generator/typescript/templates/declaration-host.ts.templ new file mode 100644 index 00000000..274b6dd2 --- /dev/null +++ b/extension/generator/typescript/templates/declaration-host.ts.templ @@ -0,0 +1,32 @@ +// Code generated by scale-extension {{ .generator_version }}, DO NOT EDIT. +// output: {{ .package_name }} + +import { Extension as ExtensionInterface } from "@loopholelabs/scale-extension-interfaces"; + +export * from "./types"; + + +export declare function New(impl: Interface): ExtensionInterface; + +// Interface to the extension impl. This is what the implementor should create + +export declare interface Interface { +{{ range $fn := .extension_schema.Functions }} + {{ $fn.Name }}(params: {{ $fn.Params }}): {{ $fn.Return }}; +{{ end }} +} + + +{{ range $ifc := .extension_schema.Interfaces }} + +export declare interface {{ $ifc.Name }} { + +{{ range $fn := $ifc.Functions }} + + {{ $fn.Name }}(params: {{ $fn.Params }}): {{ $fn.Return }}; + +{{ end }} + +} + +{{ end }} \ No newline at end of file diff --git a/extension/generator/typescript/templates/guest.ts.templ b/extension/generator/typescript/templates/guest.ts.templ new file mode 100644 index 00000000..230efb13 --- /dev/null +++ b/extension/generator/typescript/templates/guest.ts.templ @@ -0,0 +1,5 @@ +// Code generated by scale-extension {{ .generator_version }}, DO NOT EDIT. +// output: {{ .package_name }} + +/* eslint no-bitwise: off */ + diff --git a/extension/generator/typescript/templates/header.ts.templ b/extension/generator/typescript/templates/header.ts.templ new file mode 100644 index 00000000..1c34c88c --- /dev/null +++ b/extension/generator/typescript/templates/header.ts.templ @@ -0,0 +1,2 @@ +// Code generated by scale-extension {{ .generator_version }}, DO NOT EDIT. +// output: {{ .package_name }} \ No newline at end of file diff --git a/extension/generator/typescript/templates/host.ts.templ b/extension/generator/typescript/templates/host.ts.templ new file mode 100644 index 00000000..185bb67b --- /dev/null +++ b/extension/generator/typescript/templates/host.ts.templ @@ -0,0 +1,165 @@ +// Code generated by scale-extension {{ .generator_version }}, DO NOT EDIT. +// output: {{ .package_name }} + +{{ $schema := .extension_schema }} +{{ $hash := .extension_hash }} + +/* eslint no-bitwise: off */ + +import { Extension as ExtensionInterface, ModuleMemory, Resizer } from "@loopholelabs/scale-extension-interfaces"; +import { Decoder, Encoder, Kind } from "@loopholelabs/polyglot"; +import * as types from "./types"; + +export * from "./types"; + +const hash = "{{ .extension_hash }}"; + + +// Write an error to the scale function guest buffer. +function hostError(mem: ModuleMemory, resize: Resizer, err: Error) { + const enc = new Encoder(); + enc.error(err); + const ptr = resize("ext_{{ $hash }}_Resize", enc.bytes.length); + + mem.Write(ptr, enc.bytes); +} + +class hostExt { + functions: Map; + host: Host; + + constructor(fns: Map, h: Host) { + this.functions = fns; + this.host = h; + } + + Init(): Map { + return this.functions; + } + + Reset() { + // Reset any instances that have been created. + {{ range $ifc := .extension_schema.Interfaces }} + this.host.instances_{{ $ifc.Name }} = new Map(); + +// Add global functions to the runtime +{{ range $fn := .extension_schema.Functions }} + fns.set("ext_{{ $hash }}_{{ $fn.Name }}", hostWrapper.host_ext_{{ $hash }}_{{ $fn.Name }}.bind(hostWrapper)); +{{ end }} + +{{ range $ifc := .extension_schema.Interfaces }} + hostWrapper.instances_{{ $ifc.Name }} = new Map(); + + {{ range $fn := $ifc.Functions }} + + fns.set("ext_{{ $hash }}_{{ $ifc.Name }}_{{ $fn.Name }}", hostWrapper.host_ext_{{ $hash }}_{{ $ifc.Name }}_{{ $fn.Name }}.bind(hostWrapper)); + + {{ end }} +{{ end }} + + return new hostExt(fns, hostWrapper); +} + +class Host { + impl: Interface + +{{ range $ifc := .extension_schema.Interfaces }} + gid_{{ $ifc.Name }}: bigint = 0n; + instances_{{ $ifc.Name }}: Map = new Map(); +{{ end }} + + constructor(i: Interface) { + this.impl = i; + } + + // Global functions... + {{ range $fn := .extension_schema.Functions }} + + host_ext_{{ $hash }}_{{ $fn.Name}}(mem: ModuleMemory, resize: Resizer, params: number[]) { + + const d = mem.Read(params[1], params[2]); + const c = types.{{ $fn.Params }}.decode(new Decoder(d)); + const r = this.impl.{{ $fn.Name }}(c); + + {{- if (IsInterface $schema $fn.Return) }} + const id = this.gid_{{ $fn.Return }}++; + this.instances_{{ $fn.Return }}.set(id, r); + params[0] = id; + return; + {{ else }} + const enc = new Encoder(); + r.encode(enc); + const ptr = resize("ext_{{ $hash }}_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + {{ end }} + } + + {{ end }} + + // Instance functions... +{{ range $ifc := .extension_schema.Interfaces }} + +{{ range $fn := $ifc.Functions }} + + host_ext_{{ $hash }}_{{ $ifc.Name }}_{{ $fn.Name }}(mem: ModuleMemory, resize: Resizer, params: number[]): bigint { + + const d = mem.Read(params[1], params[2]); + const c = types.{{ $fn.Params }}.decode(new Decoder(d)); + + // Do lookup... + const inst = this.instances_{{ $ifc.Name }}.get(params[0]); + + const r = inst.{{ $fn.Name }}(c); + + {{- if (IsInterface $schema $fn.Return) }} + const id = this.gid_{{ $fn.Return }}++; + this.instances_{{ $fn.Return }}.set(id, r); + params[0] = id; + return; + {{ else }} + const enc = new Encoder(); + r.encode(enc); + const ptr = resize("ext_{{ $hash }}_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + {{ end }} + } +{{ end }} +{{ end }} + +} + + +//// //// //// //// //// //// //// //// //// + +// Interface to the extension impl. This is what the implementor should create + +export interface Interface { +{{ range $fn := .extension_schema.Functions }} + {{ $fn.Name }}(params: {{ $fn.Params }}): {{ $fn.Return }}; +{{ end }} +} + + +{{ range $ifc := .extension_schema.Interfaces }} + +export interface {{ $ifc.Name }} { + +{{ range $fn := $ifc.Functions }} + + {{ $fn.Name }}(params: {{ $fn.Params }}): {{ $fn.Return }}; + +{{ end }} + +} + +{{ end }} \ No newline at end of file diff --git a/extension/generator/typescript/templates/package.ts.templ b/extension/generator/typescript/templates/package.ts.templ new file mode 100644 index 00000000..76958ec1 --- /dev/null +++ b/extension/generator/typescript/templates/package.ts.templ @@ -0,0 +1,20 @@ +{ + "name": "{{ .package_name }}", + "version": "{{ .package_version }}", + "source": "index.ts", + "types": "index.d.ts", + "scripts": { + "test": "jest index.test.ts" + }, + "devDependencies": { + "@types/jest": "^29.5.2", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "typescript": "^5.1.3", + "@types/node": "^20.3.1" + }, + "dependencies": { + "@loopholelabs/polyglot": "^{{ .polyglot_version }}", + "@loopholelabs/scale-extension-interfaces": "^{{ .scale_extension_interfaces_version }}" + } +} diff --git a/extension/generator/typescript/templates/templates.go b/extension/generator/typescript/templates/templates.go new file mode 100644 index 00000000..fdfb581a --- /dev/null +++ b/extension/generator/typescript/templates/templates.go @@ -0,0 +1,19 @@ +/* + Copyright 2023 Loophole Labs + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package templates + +import "embed" + +//go:embed * +var FS embed.FS diff --git a/integration/generate_ext_test.go b/integration/generate_ext_test.go index fd4672ec..c1b89a02 100644 --- a/integration/generate_ext_test.go +++ b/integration/generate_ext_test.go @@ -24,7 +24,7 @@ import ( ) const extensionSchema = ` version = "v1alpha" - + function New { params = "stringval" return = "Example" @@ -87,9 +87,9 @@ func TestGenerateExtensionTestingSchema(t *testing.T) { Extension: s, GolangPackageImportPath: "extension", - GolangPackageName: "local_inttest_latest_guest", + GolangPackageName: "local_inttest_latest_host", - TypescriptPackageName: "local-example-latest-host", + TypescriptPackageName: "local-inttest-latest-host", TypescriptPackageVersion: "v0.1.0", }) require.NoError(t, err) @@ -101,4 +101,12 @@ func TestGenerateExtensionTestingSchema(t *testing.T) { require.NoError(t, err) } } + + typescriptExtensionDir := wd + "/typescript_ext_tests/host_extension" + for _, file := range host.TypescriptFiles { + if file.Name() != "go.mod" { + err = os.WriteFile(typescriptExtensionDir+"/"+file.Name(), file.Data(), 0644) + require.NoError(t, err) + } + } } diff --git a/integration/integration.ext.test.ts b/integration/integration.ext.test.ts new file mode 100644 index 00000000..cfd53f06 --- /dev/null +++ b/integration/integration.ext.test.ts @@ -0,0 +1,83 @@ +/* + Copyright 2022 Loophole Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import {V1BetaSchema} from "../scalefunc/scalefunc"; +import { New as NewScale } from "../scale"; + +import fs from "fs"; +import {Config} from "../config"; + +import { New as NewExtension, Interface, Example, Stringval } from "./typescript_ext_tests/host_extension"; + +import { New as NewSignature, Signature } from "./typescript_tests/host_signature"; +import {TextDecoder, TextEncoder} from "util"; + +window.TextEncoder = TextEncoder; +window.TextDecoder = TextDecoder as typeof window["TextDecoder"]; + +class ExampleImpl implements Example { + public Hello(p: Stringval): Stringval { + let sv = new Stringval(); + sv.value = "Return Hello"; + return sv; + } +} + +class ExtImpl implements Interface { + public New(p: Stringval): Example { + return new ExampleImpl(); + } + public World(p: Stringval): Stringval { + let sv = new Stringval(); + sv.value = "Return World"; + return sv; + } +} + +test("test-ext-typescript-host-rust-guest", async () => { + const impl = new ExtImpl(); + const ex = NewExtension(impl); + + const file = fs.readFileSync(process.cwd() + "/integration/rust.scale") + const sf = V1BetaSchema.Decode(file); + const config = new Config(NewSignature).WithFunction(sf).WithStdout(console.log).WithStderr(console.error).WithExtension(ex); + const s = await NewScale(config); + + const i = await s.Instance(); + const sig = NewSignature(); + + await i.Run(sig); + + expect(sig.context.stringField).toBe("This is a Rust Function. Extension New().Hello()=Return Hello World()=Return World"); +}); + +test("test-ext-typescript-host-golang-guest", async () => { + + const impl = new ExtImpl(); + const ex = NewExtension(impl); + + const file = fs.readFileSync(process.cwd() + "/integration/golang.scale") + const sf = V1BetaSchema.Decode(file); + const config = new Config(NewSignature).WithFunction(sf).WithStdout(console.log).WithStderr(console.error).WithExtension(ex) + const s = await NewScale(config); + + const i = await s.Instance(); + const sig = NewSignature(); + + await i.Run(sig); + + expect(sig.context.stringField).toBe("This is a Golang Function. Extension New().Hello()=Return Hello World()=Return World"); +}); diff --git a/integration/integration_ext_test.go b/integration/integration_ext_test.go index 8fef601f..44631099 100644 --- a/integration/integration_ext_test.go +++ b/integration/integration_ext_test.go @@ -19,6 +19,7 @@ import ( "context" "encoding/hex" "os" + "os/exec" "path/filepath" "testing" @@ -251,7 +252,7 @@ func TestExtGolangHostRustGuest(t *testing.T) { e := hostExtension.New(ext_impl) - t.Log("Starting TestGolangHostRustGuest") + t.Log("Starting TestExtGolangHostRustGuest") schema := compileExtRustGuest(t) cfg := scale.NewConfig(hostSignature.New).WithFunction(schema).WithStdout(os.Stdout).WithStderr(os.Stderr).WithExtension(e) runtime, err := scale.New(cfg) @@ -268,3 +269,43 @@ func TestExtGolangHostRustGuest(t *testing.T) { require.Equal(t, "This is a Rust Function. Extension New().Hello()=Return Hello World()=Return World", sig.Context.StringField) } + +func TestExtTypescriptHostGolangGuest(t *testing.T) { + t.Log("Starting TestExtTypescriptHostGolangGuest") + wd, err := os.Getwd() + require.NoError(t, err) + + schema := compileExtGolangGuest(t) + err = os.WriteFile(wd+"/golang.scale", schema.Encode(), 0644) + require.NoError(t, err) + t.Cleanup(func() { + err = os.Remove(wd + "/golang.scale") + require.NoError(t, err) + }) + + cmd := exec.Command("npm", "run", "test", "--", "-t", "test-ext-typescript-host-golang-guest") + cmd.Dir = wd + out, err := cmd.CombinedOutput() + assert.NoError(t, err) + t.Log(string(out)) +} + +func TestExtTypescriptHostRustGuest(t *testing.T) { + t.Log("Starting TestExtTypescriptHostRustGuest") + wd, err := os.Getwd() + require.NoError(t, err) + + schema := compileExtRustGuest(t) + err = os.WriteFile(wd+"/rust.scale", schema.Encode(), 0644) + require.NoError(t, err) + t.Cleanup(func() { + err = os.Remove(wd + "/rust.scale") + require.NoError(t, err) + }) + + cmd := exec.Command("npm", "run", "test", "--", "-t", "test-ext-typescript-host-rust-guest") + cmd.Dir = wd + out, err := cmd.CombinedOutput() + assert.NoError(t, err) + t.Log(string(out)) +} diff --git a/integration/typescript_ext_tests/host_extension/index.d.ts b/integration/typescript_ext_tests/host_extension/index.d.ts new file mode 100644 index 00000000..7e0fce6c --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/index.d.ts @@ -0,0 +1,23 @@ +// Code generated by scale-extension 0.4.5, DO NOT EDIT. +// output: local-inttest-latest-host + +import { Extension as ExtensionInterface } from "@loopholelabs/scale-extension-interfaces"; + +export * from "./types"; + +export declare function New(impl: Interface): ExtensionInterface; + +// Interface to the extension impl. This is what the implementor should create + +export declare interface Interface { + New(params: Stringval): Example; + + World(params: Stringval): Stringval; + +} + +export declare interface Example { + Hello(params: Stringval): Stringval; + +} + diff --git a/integration/typescript_ext_tests/host_extension/index.js b/integration/typescript_ext_tests/host_extension/index.js new file mode 100644 index 00000000..e421d233 --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/index.js @@ -0,0 +1,108 @@ +// Code generated by scale-extension 0.4.5, DO NOT EDIT. +// output: local-inttest-latest-host + +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var stdin_exports = {}; +__export(stdin_exports, { + New: () => New +}); +module.exports = __toCommonJS(stdin_exports); +var import_polyglot = require("@loopholelabs/polyglot"); +var types = __toESM(require("./types")); +__reExport(stdin_exports, require("./types"), module.exports); +const hash = "b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7"; +function hostError(mem, resize, err) { + const enc = new import_polyglot.Encoder(); + enc.error(err); + const ptr = resize("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); +} +class hostExt { + constructor(fns, h) { + this.functions = fns; + this.host = h; + } + Init() { + return this.functions; + } + Reset() { + this.host.instances_Example = /* @__PURE__ */ new Map() < number, Example(); + } +} +function New(impl) { + let hostWrapper = new Host(impl); + let fns = /* @__PURE__ */ new Map(); + fns.set("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New.bind(hostWrapper)); + fns.set("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World.bind(hostWrapper)); + hostWrapper.instances_Example = /* @__PURE__ */ new Map(); + fns.set("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello.bind(hostWrapper)); + return new hostExt(fns, hostWrapper); +} +class Host { + constructor(i) { + this.gid_Example = 0n; + this.instances_Example = /* @__PURE__ */ new Map(); + this.impl = i; + } + // Global functions... + host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New(mem, resize, params) { + const d = mem.Read(params[1], params[2]); + const c = types.Stringval.decode(new import_polyglot.Decoder(d)); + const r = this.impl.New(c); + const id = this.gid_Example++; + this.instances_Example.set(id, r); + params[0] = id; + return; + } + host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World(mem, resize, params) { + const d = mem.Read(params[1], params[2]); + const c = types.Stringval.decode(new import_polyglot.Decoder(d)); + const r = this.impl.World(c); + const enc = new import_polyglot.Encoder(); + r.encode(enc); + const ptr = resize("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + } + // Instance functions... + host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello(mem, resize, params) { + const d = mem.Read(params[1], params[2]); + const c = types.Stringval.decode(new import_polyglot.Decoder(d)); + const inst = this.instances_Example.get(params[0]); + const r = inst.Hello(c); + const enc = new import_polyglot.Encoder(); + r.encode(enc); + const ptr = resize("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + } +} +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/integration/typescript_ext_tests/host_extension/index.js.map b/integration/typescript_ext_tests/host_extension/index.js.map new file mode 100644 index 00000000..9d0080ab --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/index.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": [""], + "sourceRoot": "index.js", + "sourcesContent": ["// Code generated by scale-extension 0.4.5, DO NOT EDIT.\n// output: local-inttest-latest-host\n\n/* eslint no-bitwise: off */\n\nimport { Extension as ExtensionInterface, ModuleMemory, Resizer } from \"@loopholelabs/scale-extension-interfaces\";\nimport { Decoder, Encoder, Kind } from \"@loopholelabs/polyglot\";\nimport * as types from \"./types\";\n\nexport * from \"./types\";\n\nconst hash = \"b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7\";\n\n// Write an error to the scale function guest buffer.\nfunction hostError(mem: ModuleMemory, resize: Resizer, err: Error) {\n const enc = new Encoder();\n enc.error(err);\n const ptr = resize(\"ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize\", enc.bytes.length);\n\n mem.Write(ptr, enc.bytes);\n}\n\nclass hostExt {\n functions: Map;\n host: Host;\n\n constructor(fns: Map, h: Host) {\n this.functions = fns;\n this.host = h;\n }\n\n Init(): Map {\n return this.functions;\n }\n\n Reset() {\n // Reset any instances that have been created.\n this.host.instances_Example = new Map();\n\n // Add global functions to the runtime\n\n fns.set(\"ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New\", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New.bind(hostWrapper));\n\n fns.set(\"ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World\", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World.bind(hostWrapper));\n\n hostWrapper.instances_Example = new Map();\n\n fns.set(\"ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello\", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello.bind(hostWrapper));\n\n return new hostExt(fns, hostWrapper);\n}\n\nclass Host {\n impl: Interface\n\n gid_Example: bigint = 0n;\n instances_Example: Map = new Map();\n\n constructor(i: Interface) {\n this.impl = i;\n }\n\n // Global functions...\n\n host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New(mem: ModuleMemory, resize: Resizer, params: number[]) {\n const d = mem.Read(params[1], params[2]);\n const c = types.Stringval.decode(new Decoder(d));\n const r = this.impl.New(c);\n const id = this.gid_Example++;\n this.instances_Example.set(id, r);\n params[0] = id;\n return;\n }\n\n host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World(mem: ModuleMemory, resize: Resizer, params: number[]) {\n const d = mem.Read(params[1], params[2]);\n const c = types.Stringval.decode(new Decoder(d));\n const r = this.impl.World(c);\n const enc = new Encoder();\n r.encode(enc);\n const ptr = resize(\"ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize\", enc.bytes.length);\n mem.Write(ptr, enc.bytes);\n return;\n }\n\n // Instance functions...\n\n host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello(mem: ModuleMemory, resize: Resizer, params: number[]): bigint {\n const d = mem.Read(params[1], params[2]);\n const c = types.Stringval.decode(new Decoder(d));\n // Do lookup...\n const inst = this.instances_Example.get(params[0]);\n const r = inst.Hello(c);\n const enc = new Encoder();\n r.encode(enc);\n const ptr = resize(\"ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize\", enc.bytes.length);\n mem.Write(ptr, enc.bytes);\n return;\n }\n\n}\n\n//// //// //// //// //// //// //// //// ////\n\n// Interface to the extension impl. This is what the implementor should create\n\nexport interface Interface {\n New(params: Stringval): Example;\n\n World(params: Stringval): Stringval;\n\n}\n\nexport interface Example {\n Hello(params: Stringval): Stringval;\n\n}\n\n"], + "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,sBAAuC;AACvC,YAAuB;AAEvB,0BAAc,oBATd;AAWA,MAAM,OAAO;AAGb,SAAS,UAAU,KAAmB,QAAiB,KAAY;AACjE,QAAM,MAAM,IAAI,wBAAQ;AACxB,MAAI,MAAM,GAAG;AACb,QAAM,MAAM,OAAO,+EAA+E,IAAI,MAAM,MAAM;AAElH,MAAI,MAAM,KAAK,IAAI,KAAK;AAC1B;AAEA,MAAM,QAAQ;AAAA,EAIZ,YAAY,KAAmC,GAAS;AACtD,SAAK,YAAY;AACjB,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,OAAqC;AACnC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,QAAQ;AAEN,SAAK,KAAK,oBAAoB,oBAAI,QAAI,QAAQ,QAAQ;AAAA,EACxD;AACF;AAEO,SAAS,IAAI,MAAqC;AACvD,MAAI,cAAc,IAAI,KAAK,IAAI;AAE/B,MAAI,MAAM,oBAAI,IAA6B;AAI3C,MAAI,IAAI,4EAA4E,YAAY,8EAA8E,KAAK,WAAW,CAAC;AAE/L,MAAI,IAAI,8EAA8E,YAAY,gFAAgF,KAAK,WAAW,CAAC;AAEnM,cAAY,oBAAoB,oBAAI,IAAqB;AAEzD,MAAI,IAAI,sFAAsF,YAAY,wFAAwF,KAAK,WAAW,CAAC;AAEnN,SAAO,IAAI,QAAQ,KAAK,WAAW;AACrC;AAEA,MAAM,KAAK;AAAA,EAMT,YAAY,GAAc;AAH1B,uBAAsB;AACtB,6BAA0C,oBAAI,IAAqB;AAGjE,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAIA,8EAA8E,KAAmB,QAAiB,QAAkB;AAClI,UAAM,IAAI,IAAI,KAAK,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACvC,UAAM,IAAI,MAAM,UAAU,OAAO,IAAI,wBAAQ,CAAC,CAAC;AAC/C,UAAM,IAAI,KAAK,KAAK,IAAI,CAAC;AACzB,UAAM,KAAK,KAAK;AAChB,SAAK,kBAAkB,IAAI,IAAI,CAAC;AAChC,WAAO,CAAC,IAAI;AACZ;AAAA,EACF;AAAA,EAEA,gFAAgF,KAAmB,QAAiB,QAAkB;AACpI,UAAM,IAAI,IAAI,KAAK,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACvC,UAAM,IAAI,MAAM,UAAU,OAAO,IAAI,wBAAQ,CAAC,CAAC;AAC/C,UAAM,IAAI,KAAK,KAAK,MAAM,CAAC;AAC3B,UAAM,MAAM,IAAI,wBAAQ;AACxB,MAAE,OAAO,GAAG;AACZ,UAAM,MAAM,OAAO,+EAA+E,IAAI,MAAM,MAAM;AAClH,QAAI,MAAM,KAAK,IAAI,KAAK;AACxB;AAAA,EACF;AAAA;AAAA,EAIA,wFAAwF,KAAmB,QAAiB,QAA0B;AACpJ,UAAM,IAAI,IAAI,KAAK,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACvC,UAAM,IAAI,MAAM,UAAU,OAAO,IAAI,wBAAQ,CAAC,CAAC;AAE/C,UAAM,OAAO,KAAK,kBAAkB,IAAI,OAAO,CAAC,CAAC;AACjD,UAAM,IAAI,KAAK,MAAM,CAAC;AACtB,UAAM,MAAM,IAAI,wBAAQ;AACxB,MAAE,OAAO,GAAG;AACZ,UAAM,MAAM,OAAO,+EAA+E,IAAI,MAAM,MAAM;AAClH,QAAI,MAAM,KAAK,IAAI,KAAK;AACxB;AAAA,EACF;AAEF;", + "names": [] +} diff --git a/integration/typescript_ext_tests/host_extension/index.ts b/integration/typescript_ext_tests/host_extension/index.ts new file mode 100644 index 00000000..c952eee1 --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/index.ts @@ -0,0 +1,125 @@ +// Code generated by scale-extension 0.4.5, DO NOT EDIT. +// output: local-inttest-latest-host + +/* eslint no-bitwise: off */ + +import { Extension as ExtensionInterface, ModuleMemory, Resizer } from "@loopholelabs/scale-extension-interfaces"; +import { Decoder, Encoder, Kind } from "@loopholelabs/polyglot"; +import * as types from "./types"; + +export * from "./types"; + +const hash = "b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7"; + +// Write an error to the scale function guest buffer. +function hostError(mem: ModuleMemory, resize: Resizer, err: Error) { + const enc = new Encoder(); + enc.error(err); + const ptr = resize("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize", enc.bytes.length); + + mem.Write(ptr, enc.bytes); +} + +class hostExt { + functions: Map; + host: Host; + + constructor(fns: Map, h: Host) { + this.functions = fns; + this.host = h; + } + + Init(): Map { + return this.functions; + } + + Reset() { + // Reset any instances that have been created. + this.host.instances_Example = new Map(); + + // Add global functions to the runtime + + fns.set("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New.bind(hostWrapper)); + + fns.set("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World.bind(hostWrapper)); + + hostWrapper.instances_Example = new Map(); + + fns.set("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello", hostWrapper.host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello.bind(hostWrapper)); + + return new hostExt(fns, hostWrapper); +} + +class Host { + impl: Interface + + gid_Example: bigint = 0n; + instances_Example: Map = new Map(); + + constructor(i: Interface) { + this.impl = i; + } + + // Global functions... + + host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_New(mem: ModuleMemory, resize: Resizer, params: number[]) { + const d = mem.Read(params[1], params[2]); + const c = types.Stringval.decode(new Decoder(d)); + const r = this.impl.New(c); + const id = this.gid_Example++; + this.instances_Example.set(id, r); + params[0] = id; + return; + } + + host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_World(mem: ModuleMemory, resize: Resizer, params: number[]) { + const d = mem.Read(params[1], params[2]); + const c = types.Stringval.decode(new Decoder(d)); + const r = this.impl.World(c); + const enc = new Encoder(); + r.encode(enc); + const ptr = resize("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + } + + // Instance functions... + + host_ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Example_Hello(mem: ModuleMemory, resize: Resizer, params: number[]): bigint { + const d = mem.Read(params[1], params[2]); + const c = types.Stringval.decode(new Decoder(d)); + // Do lookup... + const inst = this.instances_Example.get(params[0]); + const r = inst.Hello(c); + const enc = new Encoder(); + r.encode(enc); + const ptr = resize("ext_b30af2dd8561988edd7b281ad5c1b84487072727a8ad0e490a87be0a66b037d7_Resize", enc.bytes.length); + mem.Write(ptr, enc.bytes); + return; + } + +} + +//// //// //// //// //// //// //// //// //// + +// Interface to the extension impl. This is what the implementor should create + +export interface Interface { + New(params: Stringval): Example; + + World(params: Stringval): Stringval; + +} + +export interface Example { + Hello(params: Stringval): Stringval; + +} + diff --git a/integration/typescript_ext_tests/host_extension/package.json b/integration/typescript_ext_tests/host_extension/package.json new file mode 100644 index 00000000..b633fb12 --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/package.json @@ -0,0 +1,20 @@ +{ + "name": "local-inttest-latest-host", + "version": "0.1.0", + "source": "index.ts", + "types": "index.d.ts", + "scripts": { + "test": "jest index.test.ts" + }, + "devDependencies": { + "@types/jest": "^29.5.2", + "jest": "^29.5.0", + "ts-jest": "^29.1.0", + "typescript": "^5.1.3", + "@types/node": "^20.3.1" + }, + "dependencies": { + "@loopholelabs/polyglot": "^1.1.3", + "@loopholelabs/scale-extension-interfaces": "^0.1.0" + } +} diff --git a/integration/typescript_ext_tests/host_extension/types.d.ts b/integration/typescript_ext_tests/host_extension/types.d.ts new file mode 100644 index 00000000..779c9caf --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/types.d.ts @@ -0,0 +1,29 @@ +// Code generated by scale-signature 0.4.5, DO NOT EDIT. +// output: local-inttest-latest-host + +import { Encoder, Decoder, Kind } from "@loopholelabs/polyglot" + +export declare class Stringval { + value: string; + + /** + * @throws {Error} + */ + constructor (decoder?: Decoder); + + /** + * @throws {Error} + */ + encode (encoder: Encoder); + + /** + * @throws {Error} + */ + static decode (decoder: Decoder): Stringval | undefined; + + /** + * @throws {Error} + */ + static encode_undefined (encoder: Encoder); +} + diff --git a/integration/typescript_ext_tests/host_extension/types.js b/integration/typescript_ext_tests/host_extension/types.js new file mode 100644 index 00000000..c9a1c45a --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/types.js @@ -0,0 +1,68 @@ +// Code generated by scale-signature 0.4.5, DO NOT EDIT. +// output: local-inttest-latest-host + +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var stdin_exports = {}; +__export(stdin_exports, { + Stringval: () => Stringval +}); +module.exports = __toCommonJS(stdin_exports); +class Stringval { + /** + * @throws {Error} + */ + constructor(decoder) { + if (decoder) { + let err; + try { + err = decoder.error(); + } catch (_) { + } + if (typeof err !== "undefined") { + throw err; + } + this.value = decoder.string(); + } else { + this.value = ""; + } + } + /** + * @throws {Error} + */ + encode(encoder) { + encoder.string(this.value); + } + /** + * @throws {Error} + */ + static decode(decoder) { + if (decoder.null()) { + return void 0; + } + return new Stringval(decoder); + } + /** + * @throws {Error} + */ + static encode_undefined(encoder) { + encoder.null(); + } +} +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/integration/typescript_ext_tests/host_extension/types.js.map b/integration/typescript_ext_tests/host_extension/types.js.map new file mode 100644 index 00000000..97a9569f --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/types.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": [""], + "sourceRoot": "types.js", + "sourcesContent": ["// Code generated by scale-signature 0.4.5, DO NOT EDIT.\n// output: local-inttest-latest-host\n\nimport { Encoder, Decoder, Kind } from \"@loopholelabs/polyglot\"\n\nexport class Stringval {\n value: string;\n\n /**\n * @throws {Error}\n */\n constructor (decoder?: Decoder) {\n if (decoder) {\n let err: Error | undefined;\n try {\n err = decoder.error();\n } catch (_) {}\n if (typeof err !== \"undefined\") {\n throw err;\n }\n this.value = decoder.string();\n } else {\n this.value = \"\";\n }\n }\n\n /**\n * @throws {Error}\n */\n encode (encoder: Encoder) {\n encoder.string(this.value);\n }\n\n /**\n * @throws {Error}\n */\n static decode (decoder: Decoder): Stringval | undefined {\n if (decoder.null()) {\n return undefined\n }\n return new Stringval(decoder);\n }\n\n /**\n * @throws {Error}\n */\n static encode_undefined (encoder: Encoder) {\n encoder.null();\n }\n}\n\n"], + "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAKO,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA,EAMrB,YAAa,SAAmB;AAC9B,QAAI,SAAS;AACX,UAAI;AACJ,UAAI;AACF,cAAM,QAAQ,MAAM;AAAA,MACtB,SAAS,GAAG;AAAA,MAAC;AACb,UAAI,OAAO,QAAQ,aAAa;AAC9B,cAAM;AAAA,MACR;AACA,WAAK,QAAQ,QAAQ,OAAO;AAAA,IAC9B,OAAO;AACL,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAQ,SAAkB;AACxB,YAAQ,OAAO,KAAK,KAAK;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,OAAQ,SAAyC;AACtD,QAAI,QAAQ,KAAK,GAAG;AAClB,aAAO;AAAA,IACT;AACA,WAAO,IAAI,UAAU,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,iBAAkB,SAAkB;AACzC,YAAQ,KAAK;AAAA,EACf;AACF;", + "names": [] +} diff --git a/integration/typescript_ext_tests/host_extension/types.ts b/integration/typescript_ext_tests/host_extension/types.ts new file mode 100644 index 00000000..433950a0 --- /dev/null +++ b/integration/typescript_ext_tests/host_extension/types.ts @@ -0,0 +1,51 @@ +// Code generated by scale-signature 0.4.5, DO NOT EDIT. +// output: local-inttest-latest-host + +import { Encoder, Decoder, Kind } from "@loopholelabs/polyglot" + +export class Stringval { + value: string; + + /** + * @throws {Error} + */ + constructor (decoder?: Decoder) { + if (decoder) { + let err: Error | undefined; + try { + err = decoder.error(); + } catch (_) {} + if (typeof err !== "undefined") { + throw err; + } + this.value = decoder.string(); + } else { + this.value = ""; + } + } + + /** + * @throws {Error} + */ + encode (encoder: Encoder) { + encoder.string(this.value); + } + + /** + * @throws {Error} + */ + static decode (decoder: Decoder): Stringval | undefined { + if (decoder.null()) { + return undefined + } + return new Stringval(decoder); + } + + /** + * @throws {Error} + */ + static encode_undefined (encoder: Encoder) { + encoder.null(); + } +} + diff --git a/module.ts b/module.ts index b32f71f2..35ce8736 100644 --- a/module.ts +++ b/module.ts @@ -67,12 +67,58 @@ export class Module { this.wasi = new DisabledWASI(this.template.env, stdout, stderr); this.tracing = new Tracing(this, this.template.runtime.TraceDataCallback); - const moduleConfig = { - wasi_snapshot_preview1: this.wasi.GetImports(), - scale: this.tracing.GetImports(), - env: { - next: this.template.runtime.Next(this), + const envValue:WebAssembly.ModuleImports = {}; + + envValue["next"] = this.template.runtime.Next(this); + + const mem = function(m: Module) { + return { + Write: (offset:number, v:Uint8Array): void => { + if (m.memory==undefined) { + throw new Error("no memory found in module"); + } + const writeData = new Uint8Array(m.memory.buffer); + writeData.set(v, offset); + }, + Read: (offset:number, byteCount:number): Uint8Array => { + if (m.memory==undefined) { + throw new Error("no memory found in module"); + } + const readData = new Uint8Array(m.memory.buffer); + return readData.slice(offset, offset + byteCount); }, + } + }(this) + + const resize = function(m: Module) { + return (name:string, size:number): number => { + if (m.instantiatedModule==undefined) { + throw new Error("no resize function found in module " + name); + } + const resize_function = m.instantiatedModule.exports[name] as ((len: number) => number) | undefined; + if (resize_function==undefined) { + throw new Error("no resize function found in module " + name); + } + return resize_function(size); + } + }(this); + + // Add any extensions... + for (const ext of this.template.runtime.config.extensions) { + const fns = ext.Init(); + for (const [n, fn] of fns) { + envValue[n] = (instance: number, ptr: number, len: number): bigint => { + const params: number[] = [instance, ptr, len]; + fn(mem, resize, params); + return BigInt(params[0]); + }; + } + } + + const moduleConfig = { + wasi_snapshot_preview1: this.wasi.GetImports(), + scale: this.tracing.GetImports(), + env: envValue, } this.ready = new Promise(async (resolve) => { // eslint-disable-line no-async-promise-executor diff --git a/package-lock.json b/package-lock.json index 61225a09..0047b3ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@loopholelabs/polyglot": "^1.1.3", + "@loopholelabs/scale-extension-interfaces": "^0.1.2", "@loopholelabs/scale-signature-interfaces": "^0.1.7", "buffer": "^6.0.3", "fast-sha256": "^1.3.0", @@ -1233,6 +1234,11 @@ "resolved": "https://registry.npmjs.org/@loopholelabs/polyglot/-/polyglot-1.1.3.tgz", "integrity": "sha512-PLkzZbmZJsXlmE7uTmuMwfl7QbCnafq1ibfvDE+KPrf02R72WRIml1HAVc8oAd8+bRJoEIWXjAGSN4RrxL7fyQ==" }, + "node_modules/@loopholelabs/scale-extension-interfaces": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@loopholelabs/scale-extension-interfaces/-/scale-extension-interfaces-0.1.2.tgz", + "integrity": "sha512-VArMT9SBRijRmhO+tWhHvD0N9khQsE667/Z2a4jPwgETxN9mfyzbORPGdRruE77u6vqOd6XyyAEokVFI9oJ9zQ==" + }, "node_modules/@loopholelabs/scale-signature-interfaces": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@loopholelabs/scale-signature-interfaces/-/scale-signature-interfaces-0.1.7.tgz", diff --git a/package.json b/package.json index 7782b718..49e8e6db 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@loopholelabs/polyglot": "^1.1.3", + "@loopholelabs/scale-extension-interfaces": "^0.1.2", "@loopholelabs/scale-signature-interfaces": "^0.1.7", "buffer": "^6.0.3", "fast-sha256": "^1.3.0", diff --git a/storage/extension.go b/storage/extension.go index 99e19fb4..4de5c182 100644 --- a/storage/extension.go +++ b/storage/extension.go @@ -307,6 +307,11 @@ func GenerateExtension(ext *extension.Schema, name string, tag string, org strin return err } + err = os.MkdirAll(path.Join(directory, "typescript", "host"), 0755) + if err != nil { + return err + } + guestPackage, err := generator.GenerateGuestLocal(&generator.Options{ Extension: ext, GolangPackageImportPath: fmt.Sprintf("%s_%s_%s_guest", org, name, tag), @@ -349,5 +354,17 @@ func GenerateExtension(ext *extension.Schema, name string, tag string, org strin } } + for _, file := range hostPackage.TypescriptFiles { + err = os.WriteFile(path.Join(directory, "typescript", "host", file.Path()), file.Data(), 0644) + if err != nil { + return err + } + } + + err = os.WriteFile(path.Join(directory, "typescript", "host.tar.gz"), hostPackage.TypescriptPackage.Bytes(), 0644) + if err != nil { + return err + } + return nil }