From 144892cc76463037a7b0eae1d834adb0ac082ee0 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Tue, 23 Sep 2025 22:40:56 -0500 Subject: [PATCH 01/12] Add support for importing: yaml, json, txt, png, gif, jpg, svg, webp --- internal/bundler/bundler.go | 15 ++- internal/bundler/importers.go | 242 ++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 internal/bundler/importers.go diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 9fe579b3..bbb508ad 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -280,6 +280,10 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * return err } + if err := possiblyCreateDeclarationFile(ctx.Logger, dir); err != nil { + return err + } + if err := runTypecheck(ctx, dir); err != nil { return err } @@ -334,6 +338,7 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * ctx.Logger.Debug("starting build") started := time.Now() + result := api.Build(api.BuildOptions{ EntryPoints: entryPoints, Bundle: true, @@ -347,11 +352,17 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * Engines: []api.Engine{ {Name: api.EngineNode, Version: "22"}, }, - External: []string{"bun"}, + External: []string{"bun", "fsevents"}, AbsWorkingDir: dir, TreeShaking: api.TreeShakingTrue, Drop: api.DropDebugger, - Plugins: []api.Plugin{createPlugin(ctx.Logger, dir, shimSourceMap)}, + Plugins: []api.Plugin{ + createPlugin(ctx.Logger, dir, shimSourceMap), + createYAMLImporter(ctx.Logger), + createJSONImporter(ctx.Logger), + createTextImporter(ctx.Logger), + createFileImporter(ctx.Logger), + }, Define: defines, LegalComments: api.LegalCommentsNone, Banner: map[string]string{ diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go new file mode 100644 index 00000000..2d5f23f9 --- /dev/null +++ b/internal/bundler/importers.go @@ -0,0 +1,242 @@ +package bundler + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/agentuity/go-common/logger" + cstr "github.com/agentuity/go-common/string" + "github.com/agentuity/go-common/sys" + "github.com/evanw/esbuild/pkg/api" + "gopkg.in/yaml.v3" +) + +func createYAMLImporter(logger logger.Logger) api.Plugin { + return api.Plugin{ + Name: "yaml", + Setup: func(build api.PluginBuild) { + filter := "\\.ya?ml$" + build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { + p := args.Path + abs, err := filepath.Abs(p) + if err != nil { + return api.OnResolveResult{}, err + } + if abs != p { + p = filepath.Join(args.ResolveDir, p) + } + if strings.Contains(p, "node_modules/") { + return api.OnResolveResult{}, nil + } + return api.OnResolveResult{Path: p, Namespace: "yaml"}, nil + }) + + build.OnLoad(api.OnLoadOptions{Filter: filter, Namespace: "yaml"}, func(args api.OnLoadArgs) (api.OnLoadResult, error) { + of, err := os.Open(args.Path) + if err != nil { + return api.OnLoadResult{}, err + } + defer of.Close() + kv := make(map[string]any) + err = yaml.NewDecoder(of).Decode(&kv) + if err != nil { + return api.OnLoadResult{}, err + } + js := "module.exports = " + cstr.JSONStringify(kv) + logger.Debug("bundling yaml file from %s", args.Path) + return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil + }) + + }, + } + +} + +func createJSONImporter(logger logger.Logger) api.Plugin { + return api.Plugin{ + Name: "json", + Setup: func(build api.PluginBuild) { + filter := "\\.json$" + build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { + p := args.Path + abs, err := filepath.Abs(p) + if err != nil { + return api.OnResolveResult{}, err + } + if abs != p { + p = filepath.Join(args.ResolveDir, p) + } + if strings.Contains(p, "node_modules/") { + return api.OnResolveResult{}, nil + } + return api.OnResolveResult{Path: p, Namespace: "json"}, nil + }) + + build.OnLoad(api.OnLoadOptions{Filter: filter, Namespace: "json"}, func(args api.OnLoadArgs) (api.OnLoadResult, error) { + of, err := os.Open(args.Path) + if err != nil { + return api.OnLoadResult{}, err + } + defer of.Close() + kv := make(map[string]any) + err = json.NewDecoder(of).Decode(&kv) + if err != nil { + return api.OnLoadResult{}, err + } + js := "module.exports = " + cstr.JSONStringify(kv) + logger.Debug("bundling json file from %s", args.Path) + return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil + }) + + }, + } + +} + +func createFileImporter(logger logger.Logger) api.Plugin { + return api.Plugin{ + Name: "file", + Setup: func(build api.PluginBuild) { + filter := "\\.(gif|png|jpg|jpeg|svg|webp)$" + build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { + p := args.Path + abs, err := filepath.Abs(p) + if err != nil { + return api.OnResolveResult{}, err + } + if abs != p { + p = filepath.Join(args.ResolveDir, p) + } + if strings.Contains(p, "node_modules/") { + return api.OnResolveResult{}, nil + } + return api.OnResolveResult{Path: p, Namespace: "file"}, nil + }) + + build.OnLoad(api.OnLoadOptions{Filter: filter, Namespace: "file"}, func(args api.OnLoadArgs) (api.OnLoadResult, error) { + data, err := os.ReadFile(args.Path) + if err != nil { + return api.OnLoadResult{}, err + } + base64Data := base64.StdEncoding.EncodeToString(data) + js := "module.exports = Buffer.from('" + base64Data + "', 'base64');" + logger.Debug("bundling binary file from %s", args.Path) + return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil + }) + + }, + } + +} + +func createTextImporter(logger logger.Logger) api.Plugin { + return api.Plugin{ + Name: "text", + Setup: func(build api.PluginBuild) { + filter := "\\.(txt)$" + build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "text"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { + p := args.Path + abs, err := filepath.Abs(p) + if err != nil { + return api.OnResolveResult{}, err + } + if abs != p { + p = filepath.Join(args.ResolveDir, p) + } + if strings.Contains(p, "node_modules/") { + return api.OnResolveResult{}, nil + } + return api.OnResolveResult{Path: p, Namespace: "text"}, nil + }) + + build.OnLoad(api.OnLoadOptions{Filter: filter, Namespace: "text"}, func(args api.OnLoadArgs) (api.OnLoadResult, error) { + data, err := os.ReadFile(args.Path) + if err != nil { + return api.OnLoadResult{}, err + } + js := "module.exports = " + cstr.JSONStringify(data) + logger.Debug("bundling text file from %s", args.Path) + return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil + }) + + }, + } + +} + +func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { + fp := filepath.Join(dir, "node_modules", "@types", "agentuity") + fn := filepath.Join(fp, "index.d.ts") + if sys.Exists(fn) { + logger.Debug("declaration file already exists at %s", fn) + return nil + } + if !sys.Exists(fp) { + if err := os.MkdirAll(fp, 0755); err != nil { + return fmt.Errorf("cannot create directory: %s. %w", fp, err) + } + logger.Debug("created directory %s", fp) + } + err := os.WriteFile(fn, []byte(declaration), 0644) + if err != nil { + return fmt.Errorf("cannot create file: %s. %w", fn, err) + } + logger.Debug("created declaration file at %s", fn) + return nil +} + +var declaration = ` +declare module '*.yml' { + const value: any; + export default value; +} + +declare module '*.yaml' { + const value: any; + export default value; +} + +declare module '*.json' { + const value: any; + export default value; +} + +declare module '*.png' { + const value: any; + export default value; +} + +declare module '*.gif' { + const value: any; + export default value; +} + +declare module '*.jpg' { + const value: any; + export default value; +} + +declare module '*.jpeg' { + const value: any; + export default value; +} + +declare module '*.svg' { + const value: any; + export default value; +} + +declare module '*.webp' { + const value: any; + export default value; +} + +declare module '*.txt' { + const value: any; + export default value; +} +` From 5dc7991421bc811e9f7957e6d838fd7fff467424 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Tue, 23 Sep 2025 23:15:42 -0500 Subject: [PATCH 02/12] pr feedback --- internal/bundler/bundler_test.go | 4 ++-- internal/bundler/importers.go | 28 ++++++++++++++-------------- internal/envutil/envutil_test.go | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/bundler/bundler_test.go b/internal/bundler/bundler_test.go index b2e0a546..242794c7 100644 --- a/internal/bundler/bundler_test.go +++ b/internal/bundler/bundler_test.go @@ -44,10 +44,10 @@ func testPackageManagerCommand(t *testing.T, tempDir string, runtime string, isC Logger: nil, // nil logger will skip bun lockfile generation in tests CI: isCI, } - + actualCmd, actualArgs, err := getJSInstallCommand(ctx, tempDir, runtime) require.NoError(t, err) - + assert.Equal(t, expectedCmd, actualCmd) assert.Equal(t, expectedArgs, actualArgs) } diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 2d5f23f9..3ea8a45e 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -41,12 +41,12 @@ func createYAMLImporter(logger logger.Logger) api.Plugin { return api.OnLoadResult{}, err } defer of.Close() - kv := make(map[string]any) + var kv any err = yaml.NewDecoder(of).Decode(&kv) if err != nil { return api.OnLoadResult{}, err } - js := "module.exports = " + cstr.JSONStringify(kv) + js := "export default " + cstr.JSONStringify(kv) logger.Debug("bundling yaml file from %s", args.Path) return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil }) @@ -82,12 +82,12 @@ func createJSONImporter(logger logger.Logger) api.Plugin { return api.OnLoadResult{}, err } defer of.Close() - kv := make(map[string]any) + var kv any err = json.NewDecoder(of).Decode(&kv) if err != nil { return api.OnLoadResult{}, err } - js := "module.exports = " + cstr.JSONStringify(kv) + js := "export default " + cstr.JSONStringify(kv) logger.Debug("bundling json file from %s", args.Path) return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil }) @@ -123,7 +123,7 @@ func createFileImporter(logger logger.Logger) api.Plugin { return api.OnLoadResult{}, err } base64Data := base64.StdEncoding.EncodeToString(data) - js := "module.exports = Buffer.from('" + base64Data + "', 'base64');" + js := "export default Buffer.from(" + cstr.JSONStringify(base64Data) + ", 'base64');" logger.Debug("bundling binary file from %s", args.Path) return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil }) @@ -138,7 +138,7 @@ func createTextImporter(logger logger.Logger) api.Plugin { Name: "text", Setup: func(build api.PluginBuild) { filter := "\\.(txt)$" - build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "text"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { + build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { p := args.Path abs, err := filepath.Abs(p) if err != nil { @@ -158,7 +158,7 @@ func createTextImporter(logger logger.Logger) api.Plugin { if err != nil { return api.OnLoadResult{}, err } - js := "module.exports = " + cstr.JSONStringify(data) + js := "export default " + cstr.JSONStringify(string(data)) logger.Debug("bundling text file from %s", args.Path) return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil }) @@ -206,37 +206,37 @@ declare module '*.json' { } declare module '*.png' { - const value: any; + const value: Buffer; export default value; } declare module '*.gif' { - const value: any; + const value: Buffer; export default value; } declare module '*.jpg' { - const value: any; + const value: Buffer; export default value; } declare module '*.jpeg' { - const value: any; + const value: Buffer; export default value; } declare module '*.svg' { - const value: any; + const value: Buffer; export default value; } declare module '*.webp' { - const value: any; + const value: Buffer; export default value; } declare module '*.txt' { - const value: any; + const value: string; export default value; } ` diff --git a/internal/envutil/envutil_test.go b/internal/envutil/envutil_test.go index b86fdc3d..8b73a8df 100644 --- a/internal/envutil/envutil_test.go +++ b/internal/envutil/envutil_test.go @@ -81,9 +81,9 @@ func TestIsAgentuityEnv(t *testing.T) { func TestShouldSyncToProduction(t *testing.T) { tests := []struct { - name string - isLocalDev bool - shouldSync bool + name string + isLocalDev bool + shouldSync bool }{ { name: "production mode should sync", From 0ec762afb2664cd38b2347620dcc250f31cc388e Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Tue, 23 Sep 2025 23:22:03 -0500 Subject: [PATCH 03/12] use util function --- internal/bundler/importers.go | 46 +++++++++++------------------------ 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 3ea8a45e..24c8b288 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -15,20 +15,23 @@ import ( "gopkg.in/yaml.v3" ) +func makePath(args api.OnResolveArgs) string { + p := args.Path + if !filepath.IsAbs(p) { + p = filepath.Join(args.ResolveDir, p) + } else { + p = filepath.Clean(p) + } + return p +} + func createYAMLImporter(logger logger.Logger) api.Plugin { return api.Plugin{ Name: "yaml", Setup: func(build api.PluginBuild) { filter := "\\.ya?ml$" build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { - p := args.Path - abs, err := filepath.Abs(p) - if err != nil { - return api.OnResolveResult{}, err - } - if abs != p { - p = filepath.Join(args.ResolveDir, p) - } + p := makePath(args) if strings.Contains(p, "node_modules/") { return api.OnResolveResult{}, nil } @@ -62,14 +65,7 @@ func createJSONImporter(logger logger.Logger) api.Plugin { Setup: func(build api.PluginBuild) { filter := "\\.json$" build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { - p := args.Path - abs, err := filepath.Abs(p) - if err != nil { - return api.OnResolveResult{}, err - } - if abs != p { - p = filepath.Join(args.ResolveDir, p) - } + p := makePath(args) if strings.Contains(p, "node_modules/") { return api.OnResolveResult{}, nil } @@ -103,14 +99,7 @@ func createFileImporter(logger logger.Logger) api.Plugin { Setup: func(build api.PluginBuild) { filter := "\\.(gif|png|jpg|jpeg|svg|webp)$" build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { - p := args.Path - abs, err := filepath.Abs(p) - if err != nil { - return api.OnResolveResult{}, err - } - if abs != p { - p = filepath.Join(args.ResolveDir, p) - } + p := makePath(args) if strings.Contains(p, "node_modules/") { return api.OnResolveResult{}, nil } @@ -139,14 +128,7 @@ func createTextImporter(logger logger.Logger) api.Plugin { Setup: func(build api.PluginBuild) { filter := "\\.(txt)$" build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { - p := args.Path - abs, err := filepath.Abs(p) - if err != nil { - return api.OnResolveResult{}, err - } - if abs != p { - p = filepath.Join(args.ResolveDir, p) - } + p := makePath(args) if strings.Contains(p, "node_modules/") { return api.OnResolveResult{}, nil } From 4bd8c9dc87dae12fc9084add78d90e078f56a7f9 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 09:50:45 -0500 Subject: [PATCH 04/12] support for more text formats --- internal/bundler/importers.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 21a9493b..af2f3142 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -130,7 +130,7 @@ func createTextImporter(logger logger.Logger) api.Plugin { return api.Plugin{ Name: "text", Setup: func(build api.PluginBuild) { - filter := "\\.(txt)$" + filter := "\\.(txt|md|csv|xml|sql)$" build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { p := makePath(args) if isNodeModulesPath(p) { @@ -225,4 +225,24 @@ declare module '*.txt' { const value: string; export default value; } + +declare module '*.md' { + const value: string; + export default value; +} + +declare module '*.csv' { + const value: string; + export default value; +} + +declare module '*.xml' { + const value: string; + export default value; +} + +declare module '*.sql' { + const value: string; + export default value; +} ` From 0bbf6f5841364a07b1d47b8672fd19675bc1c578 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 09:57:08 -0500 Subject: [PATCH 05/12] PDF support --- internal/bundler/importers.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index af2f3142..423063cc 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -101,7 +101,7 @@ func createFileImporter(logger logger.Logger) api.Plugin { return api.Plugin{ Name: "file", Setup: func(build api.PluginBuild) { - filter := "\\.(gif|png|jpg|jpeg|svg|webp)$" + filter := "\\.(gif|png|jpg|jpeg|svg|webp|pdf)$" build.OnResolve(api.OnResolveOptions{Filter: filter, Namespace: "file"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { p := makePath(args) if isNodeModulesPath(p) { @@ -221,6 +221,11 @@ declare module '*.webp' { export default value; } +declare module '*.pdf' { + const value: Buffer; + export default value; +} + declare module '*.txt' { const value: string; export default value; From ddc44b6c74b95ac6026d3e18beca31fe7f2bf509 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 10:16:48 -0500 Subject: [PATCH 06/12] switch to use non-node specific buffer --- internal/bundler/importers.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 423063cc..803a3ee0 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -116,7 +116,7 @@ func createFileImporter(logger logger.Logger) api.Plugin { return api.OnLoadResult{}, err } base64Data := base64.StdEncoding.EncodeToString(data) - js := "export default Buffer.from(" + cstr.JSONStringify(base64Data) + ", 'base64');" + js := "export default new Uint8Array(atob(" + cstr.JSONStringify(base64Data) + ").split('').map(c => c.charCodeAt(0)));" logger.Debug("bundling binary file from %s", args.Path) return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil }) @@ -192,37 +192,37 @@ declare module '*.json' { } declare module '*.png' { - const value: Buffer; + const value: Uint8Array; export default value; } declare module '*.gif' { - const value: Buffer; + const value: Uint8Array; export default value; } declare module '*.jpg' { - const value: Buffer; + const value: Uint8Array; export default value; } declare module '*.jpeg' { - const value: Buffer; + const value: Uint8Array; export default value; } declare module '*.svg' { - const value: Buffer; + const value: Uint8Array; export default value; } declare module '*.webp' { - const value: Buffer; + const value: Uint8Array; export default value; } declare module '*.pdf' { - const value: Buffer; + const value: Uint8Array; export default value; } From 6c7a2103905c08b051841d55d1018180ad0a7ede Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 12:00:36 -0500 Subject: [PATCH 07/12] write the declaration file into the sdk dist folder in case they types is used --- internal/bundler/importers.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 803a3ee0..d6bb78f8 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -155,6 +155,11 @@ func createTextImporter(logger logger.Logger) api.Plugin { } func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { + mfp := filepath.Join(dir, "node_modules", "@agentuity", "sdk", "dist", "file_types.d.ts") + if sys.Exists(mfp) { + logger.Debug("found existing declaration file at %s", mfp) + return nil + } fp := filepath.Join(dir, "node_modules", "@types", "agentuity") fn := filepath.Join(fp, "index.d.ts") if sys.Exists(fn) { @@ -171,6 +176,11 @@ func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { if err != nil { return fmt.Errorf("cannot create file: %s. %w", fn, err) } + logger.Debug("created declaration file at %s", mfp) + err = os.WriteFile(fn, []byte(mfp), 0644) + if err != nil { + return fmt.Errorf("cannot create file: %s. %w", fn, err) + } logger.Debug("created declaration file at %s", fn) return nil } From 31475d433529ad0b698d89ab484c66afd3b602ff Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 15:12:27 -0500 Subject: [PATCH 08/12] fix --- internal/bundler/importers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index d6bb78f8..8105f399 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -177,7 +177,7 @@ func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { return fmt.Errorf("cannot create file: %s. %w", fn, err) } logger.Debug("created declaration file at %s", mfp) - err = os.WriteFile(fn, []byte(mfp), 0644) + err = os.WriteFile(mfp, []byte(declaration), 0644) if err != nil { return fmt.Errorf("cannot create file: %s. %w", fn, err) } From eb9de09bcd9ee8f6479fde6dd972f35b3c05f264 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 15:56:08 -0500 Subject: [PATCH 09/12] more fixes around bundle --- internal/bundler/importers.go | 96 ++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 8105f399..2f995021 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -156,32 +156,94 @@ func createTextImporter(logger logger.Logger) api.Plugin { func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { mfp := filepath.Join(dir, "node_modules", "@agentuity", "sdk", "dist", "file_types.d.ts") - if sys.Exists(mfp) { + fileExists := sys.Exists(mfp) + if fileExists { logger.Debug("found existing declaration file at %s", mfp) - return nil } fp := filepath.Join(dir, "node_modules", "@types", "agentuity") fn := filepath.Join(fp, "index.d.ts") - if sys.Exists(fn) { + typesExists := sys.Exists(fn) + if typesExists { logger.Debug("declaration file already exists at %s", fn) - return nil } - if !sys.Exists(fp) { - if err := os.MkdirAll(fp, 0755); err != nil { - return fmt.Errorf("cannot create directory: %s. %w", fp, err) + + // Only create files if they don't exist + if !fileExists || !typesExists { + if !sys.Exists(fp) { + if err := os.MkdirAll(fp, 0755); err != nil { + return fmt.Errorf("cannot create directory: %s. %w", fp, err) + } + logger.Debug("created directory %s", fp) + } + + // Create the @types/agentuity declaration file + if !typesExists { + err := os.WriteFile(fn, []byte(declaration), 0644) + if err != nil { + return fmt.Errorf("cannot create file: %s. %w", fn, err) + } + logger.Debug("created declaration file at %s", fn) + } + + // Create the SDK file_types.d.ts + if !fileExists { + err := os.WriteFile(mfp, []byte(declaration), 0644) + if err != nil { + return fmt.Errorf("cannot create file: %s. %w", mfp, err) + } + logger.Debug("created declaration file at %s", mfp) } - logger.Debug("created directory %s", fp) - } - err := os.WriteFile(fn, []byte(declaration), 0644) - if err != nil { - return fmt.Errorf("cannot create file: %s. %w", fn, err) } - logger.Debug("created declaration file at %s", mfp) - err = os.WriteFile(mfp, []byte(declaration), 0644) - if err != nil { - return fmt.Errorf("cannot create file: %s. %w", fn, err) + + // Patch the SDK's main index.d.ts to import file_types if it exists and doesn't already import it + sdkIndexPath := filepath.Join(dir, "node_modules", "@agentuity", "sdk", "dist", "index.d.ts") + if sys.Exists(sdkIndexPath) { + content, err := os.ReadFile(sdkIndexPath) + if err == nil { + contentStr := string(content) + // Only add the import if it's not already there + if !strings.Contains(contentStr, "import './file_types'") && !strings.Contains(contentStr, "import \"./file_types\"") { + // Find where to insert the import (after the existing exports) + lines := strings.Split(contentStr, "\n") + var newLines []string + inserted := false + for _, line := range lines { + newLines = append(newLines, line) + // Insert after the last export statement, before other imports + if !inserted && strings.HasPrefix(strings.TrimSpace(line), "export ") && + (strings.Contains(line, "from './") || strings.Contains(line, "from \"./")) { + newLines = append(newLines, "import './file_types';") + inserted = true + } + } + // If we didn't insert it yet, add it after the exports + if !inserted && len(newLines) > 0 { + // Find the position after existing exports + for i, line := range newLines { + if strings.HasPrefix(strings.TrimSpace(line), "export ") { + continue + } + // Insert before the first non-export line + newContent := append(newLines[:i], append([]string{"import './file_types';"}, newLines[i:]...)...) + contentStr = strings.Join(newContent, "\n") + inserted = true + break + } + } + if inserted { + contentStr = strings.Join(newLines, "\n") + } + + err = os.WriteFile(sdkIndexPath, []byte(contentStr), 0644) + if err != nil { + logger.Debug("failed to patch SDK index.d.ts: %v", err) + } else { + logger.Debug("patched SDK index.d.ts to include file_types import") + } + } + } } - logger.Debug("created declaration file at %s", fn) + return nil } From 10b5a374172a445dc0c04da936faa1b0c8e5f40f Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 16:03:29 -0500 Subject: [PATCH 10/12] use Buffer wrapped into Uint8Array --- internal/bundler/importers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 2f995021..369fc7f3 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -116,7 +116,7 @@ func createFileImporter(logger logger.Logger) api.Plugin { return api.OnLoadResult{}, err } base64Data := base64.StdEncoding.EncodeToString(data) - js := "export default new Uint8Array(atob(" + cstr.JSONStringify(base64Data) + ").split('').map(c => c.charCodeAt(0)));" + js := "export default new Uint8Array(Buffer.from(" + cstr.JSONStringify(base64Data) + ", \"base64\"));" logger.Debug("bundling binary file from %s", args.Path) return api.OnLoadResult{Contents: &js, Loader: api.LoaderJS}, nil }) From a5c95d66434760d2474bd6f238de7057a2603ff0 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 16:09:08 -0500 Subject: [PATCH 11/12] add smart update logic --- internal/bundler/importers.go | 100 +++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 369fc7f3..071ac07d 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -1,6 +1,7 @@ package bundler import ( + "crypto/sha256" "encoding/base64" "encoding/json" "fmt" @@ -154,45 +155,90 @@ func createTextImporter(logger logger.Logger) api.Plugin { } +func needsDeclarationUpdate(filePath string, expectedHash string) bool { + file, err := os.Open(filePath) + if err != nil { + return true // File doesn't exist or can't be read, needs update + } + defer file.Close() + + // Read first 100 bytes to check for hash comment + buffer := make([]byte, 100) + n, err := file.Read(buffer) + if err != nil || n == 0 { + return true // Can't read file, needs update + } + + content := string(buffer[:n]) + lines := strings.Split(content, "\n") + if len(lines) == 0 { + return true + } + + // Check if first line contains our hash + firstLine := strings.TrimSpace(lines[0]) + expectedPrefix := "// agentuity-types-hash:" + if !strings.HasPrefix(firstLine, expectedPrefix) { + return true // No hash found, needs update + } + + currentHash := strings.TrimPrefix(firstLine, expectedPrefix) + return currentHash != expectedHash +} + func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { + // Generate hash of declaration content + hash := fmt.Sprintf("%x", sha256.Sum256([]byte(declaration))) + + // Create declaration with hash header + declarationWithHash := fmt.Sprintf("// agentuity-types-hash:%s\n%s", hash, declaration) + mfp := filepath.Join(dir, "node_modules", "@agentuity", "sdk", "dist", "file_types.d.ts") - fileExists := sys.Exists(mfp) - if fileExists { - logger.Debug("found existing declaration file at %s", mfp) - } fp := filepath.Join(dir, "node_modules", "@types", "agentuity") fn := filepath.Join(fp, "index.d.ts") - typesExists := sys.Exists(fn) - if typesExists { - logger.Debug("declaration file already exists at %s", fn) + + // Check if files need updates + mfpNeedsUpdate := needsDeclarationUpdate(mfp, hash) + fnNeedsUpdate := needsDeclarationUpdate(fn, hash) + + if !mfpNeedsUpdate && !fnNeedsUpdate { + logger.Debug("declaration files are up to date") + return nil } - // Only create files if they don't exist - if !fileExists || !typesExists { - if !sys.Exists(fp) { - if err := os.MkdirAll(fp, 0755); err != nil { - return fmt.Errorf("cannot create directory: %s. %w", fp, err) - } - logger.Debug("created directory %s", fp) + // Create directory if needed + if !sys.Exists(fp) { + if err := os.MkdirAll(fp, 0755); err != nil { + return fmt.Errorf("cannot create directory: %s. %w", fp, err) } + logger.Debug("created directory %s", fp) + } - // Create the @types/agentuity declaration file - if !typesExists { - err := os.WriteFile(fn, []byte(declaration), 0644) - if err != nil { - return fmt.Errorf("cannot create file: %s. %w", fn, err) - } - logger.Debug("created declaration file at %s", fn) + // Create/update the @types/agentuity declaration file + if fnNeedsUpdate { + err := os.WriteFile(fn, []byte(declarationWithHash), 0644) + if err != nil { + return fmt.Errorf("cannot create file: %s. %w", fn, err) } + logger.Debug("updated declaration file at %s", fn) + } - // Create the SDK file_types.d.ts - if !fileExists { - err := os.WriteFile(mfp, []byte(declaration), 0644) - if err != nil { - return fmt.Errorf("cannot create file: %s. %w", mfp, err) + // Create/update the SDK file_types.d.ts + if mfpNeedsUpdate { + // Ensure SDK directory exists + sdkDir := filepath.Dir(mfp) + if !sys.Exists(sdkDir) { + if err := os.MkdirAll(sdkDir, 0755); err != nil { + return fmt.Errorf("cannot create SDK directory: %s. %w", sdkDir, err) } - logger.Debug("created declaration file at %s", mfp) + logger.Debug("created SDK directory %s", sdkDir) + } + + err := os.WriteFile(mfp, []byte(declarationWithHash), 0644) + if err != nil { + return fmt.Errorf("cannot create file: %s. %w", mfp, err) } + logger.Debug("updated declaration file at %s", mfp) } // Patch the SDK's main index.d.ts to import file_types if it exists and doesn't already import it From a6287be3c74ee75cdec38a5b2d268bf4d0dc4205 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Wed, 24 Sep 2025 16:48:59 -0500 Subject: [PATCH 12/12] add tests --- internal/bundler/importers.go | 14 +- internal/bundler/importers_test.go | 242 +++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 internal/bundler/importers_test.go diff --git a/internal/bundler/importers.go b/internal/bundler/importers.go index 071ac07d..776cffb7 100644 --- a/internal/bundler/importers.go +++ b/internal/bundler/importers.go @@ -249,13 +249,14 @@ func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { contentStr := string(content) // Only add the import if it's not already there if !strings.Contains(contentStr, "import './file_types'") && !strings.Contains(contentStr, "import \"./file_types\"") { - // Find where to insert the import (after the existing exports) + // Find where to insert the import (after the first relative export) lines := strings.Split(contentStr, "\n") var newLines []string inserted := false + for _, line := range lines { newLines = append(newLines, line) - // Insert after the last export statement, before other imports + // Insert after the first export with relative import if !inserted && strings.HasPrefix(strings.TrimSpace(line), "export ") && (strings.Contains(line, "from './") || strings.Contains(line, "from \"./")) { newLines = append(newLines, "import './file_types';") @@ -276,10 +277,15 @@ func possiblyCreateDeclarationFile(logger logger.Logger, dir string) error { break } } - if inserted { + // Update contentStr with the modified lines if we inserted something in the first loop + if inserted && contentStr == string(content) { + contentStr = strings.Join(newLines, "\n") + } else if !inserted && len(newLines) > 0 { + // If we still haven't inserted and there are only exports, append at the end + newLines = append(newLines, "import './file_types';") contentStr = strings.Join(newLines, "\n") } - + err = os.WriteFile(sdkIndexPath, []byte(contentStr), 0644) if err != nil { logger.Debug("failed to patch SDK index.d.ts: %v", err) diff --git a/internal/bundler/importers_test.go b/internal/bundler/importers_test.go new file mode 100644 index 00000000..ab0ea9b8 --- /dev/null +++ b/internal/bundler/importers_test.go @@ -0,0 +1,242 @@ +package bundler + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/agentuity/go-common/logger" +) + +// Mock logger for testing +type mockLogger struct{} + +func (m *mockLogger) Trace(format string, args ...interface{}) {} +func (m *mockLogger) Debug(format string, args ...interface{}) {} +func (m *mockLogger) Info(format string, args ...interface{}) {} +func (m *mockLogger) Warn(format string, args ...interface{}) {} +func (m *mockLogger) Error(format string, args ...interface{}) {} +func (m *mockLogger) Fatal(format string, args ...interface{}) {} +func (m *mockLogger) IsTraceEnabled() bool { return false } +func (m *mockLogger) IsDebugEnabled() bool { return false } +func (m *mockLogger) IsInfoEnabled() bool { return false } +func (m *mockLogger) IsWarnEnabled() bool { return false } +func (m *mockLogger) IsErrorEnabled() bool { return false } +func (m *mockLogger) IsFatalEnabled() bool { return false } +func (m *mockLogger) WithField(key string, value interface{}) logger.Logger { return m } +func (m *mockLogger) WithFields(fields map[string]interface{}) logger.Logger { return m } +func (m *mockLogger) WithError(err error) logger.Logger { return m } +func (m *mockLogger) Stack(logger logger.Logger) logger.Logger { return m } +func (m *mockLogger) With(fields map[string]interface{}) logger.Logger { return m } +func (m *mockLogger) WithContext(ctx context.Context) logger.Logger { return m } +func (m *mockLogger) WithPrefix(prefix string) logger.Logger { return m } + +func TestImportInsertion(t *testing.T) { + tests := []struct { + name string + inputContent string + expectedOutput string + shouldInsert bool + }{ + { + name: "insert after export with relative import", + inputContent: `export { Tool } from './tool'; +export { Agent } from './agent'; +declare module '@agentuity/sdk' { + export interface Config {} +}`, + expectedOutput: `export { Tool } from './tool'; +import './file_types'; +export { Agent } from './agent'; +declare module '@agentuity/sdk' { + export interface Config {} +}`, + shouldInsert: true, + }, + { + name: "insert after last export when no relative imports", + inputContent: `export interface Tool {} +export class Agent {} +declare module '@agentuity/sdk' { + export interface Config {} +}`, + expectedOutput: `export interface Tool {} +export class Agent {} +import './file_types'; +declare module '@agentuity/sdk' { + export interface Config {} +}`, + shouldInsert: true, + }, + { + name: "don't insert when import already exists with single quotes", + inputContent: `export { Tool } from './tool'; +import './file_types'; +export { Agent } from './agent';`, + expectedOutput: `export { Tool } from './tool'; +import './file_types'; +export { Agent } from './agent';`, + shouldInsert: false, + }, + { + name: "don't insert when import already exists with double quotes", + inputContent: `export { Tool } from "./tool"; +import "./file_types"; +export { Agent } from "./agent";`, + expectedOutput: `export { Tool } from "./tool"; +import "./file_types"; +export { Agent } from "./agent";`, + shouldInsert: false, + }, + { + name: "append at end when only exports exist", + inputContent: `export interface Tool {} +export class Agent {}`, + expectedOutput: `export interface Tool {} +export class Agent {} +import './file_types';`, + shouldInsert: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory and files + tmpDir, err := os.MkdirTemp("", "bundler_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create SDK directory structure + sdkDir := filepath.Join(tmpDir, "node_modules", "@agentuity", "sdk", "dist") + err = os.MkdirAll(sdkDir, 0755) + if err != nil { + t.Fatalf("failed to create SDK dir: %v", err) + } + + // Write test content to index.d.ts + indexPath := filepath.Join(sdkDir, "index.d.ts") + err = os.WriteFile(indexPath, []byte(tt.inputContent), 0644) + if err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + + + // Also need to create the file_types.d.ts file that the patching logic expects to exist + // This triggers the SDK patching logic + fileTypesPath := filepath.Join(sdkDir, "file_types.d.ts") + err = os.WriteFile(fileTypesPath, []byte("// placeholder"), 0644) + if err != nil { + t.Fatalf("failed to write file_types.d.ts: %v", err) + } + + // Create a mock logger + mockLog := &mockLogger{} + + // Call the function under test + err = possiblyCreateDeclarationFile(mockLog, tmpDir) + if err != nil { + t.Fatalf("possiblyCreateDeclarationFile failed: %v", err) + } + + // Read the result + result, err := os.ReadFile(indexPath) + if err != nil { + t.Fatalf("failed to read result file: %v", err) + } + + + + resultStr := string(result) + if resultStr != tt.expectedOutput { + t.Errorf("unexpected output\nexpected:\n%s\n\nactual:\n%s", tt.expectedOutput, resultStr) + } + + // Verify import detection logic + hasImport := strings.Contains(resultStr, "import './file_types'") || strings.Contains(resultStr, "import \"./file_types\"") + if tt.shouldInsert && !hasImport { + t.Errorf("expected import to be inserted but it wasn't found") + } + if !tt.shouldInsert && hasImport && !strings.Contains(tt.inputContent, "file_types") { + t.Errorf("expected import not to be inserted but it was added") + } + }) + } +} + +func TestNeedsDeclarationUpdate(t *testing.T) { + tests := []struct { + name string + fileContent string + expectedHash string + shouldUpdate bool + }{ + { + name: "file doesn't exist", + fileContent: "", + expectedHash: "abc123", + shouldUpdate: true, + }, + { + name: "file has matching hash", + fileContent: "// agentuity-types-hash:abc123\ndeclare module '*.yml' {}", + expectedHash: "abc123", + shouldUpdate: false, + }, + { + name: "file has different hash", + fileContent: "// agentuity-types-hash:def456\ndeclare module '*.yml' {}", + expectedHash: "abc123", + shouldUpdate: true, + }, + { + name: "file has no hash", + fileContent: "declare module '*.yml' {}", + expectedHash: "abc123", + shouldUpdate: true, + }, + { + name: "empty file", + fileContent: "", + expectedHash: "abc123", + shouldUpdate: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.name == "file doesn't exist" { + // Test non-existent file + result := needsDeclarationUpdate("/nonexistent/path", tt.expectedHash) + if result != tt.shouldUpdate { + t.Errorf("expected %v, got %v", tt.shouldUpdate, result) + } + return + } + + // Create temporary file + tmpFile, err := os.CreateTemp("", "test_declaration") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Write test content + _, err = tmpFile.WriteString(tt.fileContent) + if err != nil { + t.Fatalf("failed to write test content: %v", err) + } + tmpFile.Close() + + // Test the function + result := needsDeclarationUpdate(tmpFile.Name(), tt.expectedHash) + if result != tt.shouldUpdate { + t.Errorf("expected %v, got %v for content: %s", tt.shouldUpdate, result, tt.fileContent) + } + }) + } +}