diff --git a/README.md b/README.md index 18a8fa9..5e87265 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,18 @@ [![Go Version](https://img.shields.io/badge/Go-1.26.0-00ADD8?logo=go)](https://go.dev/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) -`flare-edge-cli` scaffolds, validates, builds, deploys, tails, and tears down Go-for-Wasm Cloudflare Workers projects. It generates the Worker shim and Wrangler config, runs compatibility checks against a Workers/Wasm profile, and delegates Cloudflare operations to Wrangler and Cloudflare APIs where appropriate. +`flare-edge-cli` scaffolds, validates, builds, deploys, tails, and tears down Cloudflare Workers projects. It supports both Go-for-Wasm Workers and native JavaScript module Workers, generates Wrangler config, runs Go compatibility checks where appropriate, and delegates Cloudflare operations to Wrangler and Cloudflare APIs where appropriate. -This CLI is designed first for AI agents. Humans can use it directly, but the primary goal is to give coding agents a stable, scriptable control surface for creating and operating Cloudflare edge functions and lightweight microservices in Go. +This CLI is designed first for AI agents. Humans can use it directly, but the primary goal is to give coding agents a stable, scriptable control surface for creating and operating Cloudflare edge functions and lightweight microservices in Go or native JavaScript. ## Why This Tool Exists -`flare-edge-cli` exists to standardize the end-to-end agent workflow for Go on Cloudflare: +`flare-edge-cli` exists to standardize the end-to-end agent workflow for Cloudflare Workers: - scaffold a deployable project with deterministic structure - validate whether the Go code fits Workers/Wasm constraints -- build the `.wasm` artifact and Worker shim correctly every time +- build the `.wasm` artifact and Worker shim correctly for Go projects +- stage native JavaScript Worker entrypoints without changing the deploy flow - provision and manage Cloudflare resources through one consistent interface - emit machine-readable output that agents can inspect and chain into later actions - tear down remote and local side effects when an ephemeral environment is no longer needed @@ -29,7 +30,7 @@ The intended operator is usually an AI agent acting on behalf of a developer. Be ## Primary Use Case -The primary use case is simple: an AI agent needs a standard way to create and deploy an edge function or small microservice on Cloudflare using Go without rebuilding the same scaffolding, compatibility analysis, build orchestration, deployment logic, and cleanup flow for every task. +The primary use case is simple: an AI agent needs a standard way to create and deploy an edge function or small microservice on Cloudflare without rebuilding the same scaffolding, compatibility analysis, build orchestration, deployment logic, and cleanup flow for every task. This means `flare-edge-cli` is not just a deploy wrapper. It is an agent-oriented execution surface for: @@ -43,9 +44,10 @@ This means `flare-edge-cli` is not just a deploy wrapper. It is an agent-oriente ## What It Does -- Scaffolds Go Worker projects with a reproducible layout +- Scaffolds Go/Wasm and native JavaScript Worker projects with a reproducible layout - Validates Go code against a Workers/Wasm compatibility profile -- Builds `.wasm` artifacts and the JavaScript Worker shim +- Builds `.wasm` artifacts and the JavaScript Worker shim for Go projects +- Stages native JavaScript Worker entrypoints for deploy, route, and doctor workflows - Runs local or remote dev sessions through Wrangler - Configures Workers AI bindings for Go-based AI Workers - Deploys versioned Workers and manages routes, secrets, KV, D1, R2, and releases @@ -65,10 +67,11 @@ The implementation is intentionally biased toward agent use: ## Requirements -- Go `1.26.0` +- Go `1.26.0` for Go/Wasm projects - [Wrangler](https://developers.cloudflare.com/workers/wrangler/) installed and available on `PATH` - Cloudflare authentication already configured through Wrangler or an API token - Optional: TinyGo for `--tinygo` builds +- Optional: Cloudflare `nodejs_compat` compatibility flag for JavaScript Workers via `project init --node-compat` ## Install @@ -123,12 +126,18 @@ go run ./cmd/flare-edge-cli --help ## Typical Workflow -Initialize a project: +Initialize a Go project: ```bash ./flare-edge-cli project init my-worker --module-path github.com/example/my-worker ``` +Initialize a JavaScript Worker: + +```bash +./flare-edge-cli project init my-js-worker --runtime js +``` + Check compatibility: ```bash @@ -161,7 +170,9 @@ Clean everything up later: ## Generated Project Layout -`project init` creates a Go Worker project with these important files: +`project init` creates runtime-specific Worker projects. + +Go/Wasm projects: ```text / @@ -182,6 +193,17 @@ Important generated paths: - `dist/app.wasm`: compiled artifact after build - `dist/worker.mjs`: Worker shim after build +JavaScript projects: + +```text +/ + flare-edge.json + wrangler.jsonc + src/worker.mjs + README.md + .gitignore +``` + ## Configuration Files ### `flare-edge.json` @@ -189,10 +211,11 @@ Important generated paths: This is the CLI’s typed project metadata file. It tracks: - project and module names -- Go entrypoint +- runtime, entrypoint, and deployed main module - output directory and artifact names - Worker name - compatibility date and profile +- optional Node.js compatibility flag - bindings for KV, D1, R2, vars, and secrets - generated shim metadata @@ -254,12 +277,13 @@ Notes: Create and inspect projects. ```bash -./flare-edge-cli project init [--cwd ] [--module-path ] [--package ] [--template ] [--compat-date ] [--env ] [--use-jsonc] [--with-git] [--yes] +./flare-edge-cli project init [--cwd ] [--runtime go|js] [--module-path ] [--package ] [--template ] [--compat-date ] [--node-compat] [--env ] [--use-jsonc] [--with-git] [--yes] ./flare-edge-cli project info [--cwd ] [--json] [--show-generated] [--show-bindings] ``` Supported templates: +- `js-worker` - `edge-http` - `edge-json` - `scheduled` @@ -284,6 +308,11 @@ AI templates: - `ai-image`: text-to-image scaffold using `@cf/black-forest-labs/flux-2-klein-9b` - `ai-embeddings`: embeddings scaffold using `@cf/qwen/qwen3-embedding-0.6b` +Notes: + +- `--runtime js` defaults to the `js-worker` template +- `--node-compat` only adds Wrangler's `nodejs_compat` compatibility flag; it does not switch the runtime to Node.js automatically + ### `ai` Manage the Workers AI binding stored in local project config. @@ -304,15 +333,15 @@ Notes: Run the Go/Wasm compatibility analyzer or inspect built-in rules. ```bash -./flare-edge-cli compat check [--path ] [--entry ] [--profile worker-wasm] [--strict] [--json] [--sarif] [--fail-on warning|error] [--exclude ] +./flare-edge-cli compat check [--path ] [--entry ] [--profile ] [--strict] [--json] [--sarif] [--fail-on warning|error] [--exclude ] ./flare-edge-cli compat rules [--json] [--severity error|warning|info] ``` -Diagnostics include structured fields such as rule ID, severity, file, line, message, why, and fix hint. +Diagnostics include structured fields such as rule ID, severity, file, line, message, why, and fix hint. JavaScript Worker projects skip Go static analysis and return an empty result. ### `build` -Compile Go to Wasm and inspect artifacts. +Build Go/Wasm projects or stage JavaScript Workers for deploy. ```bash ./flare-edge-cli build [--path ] [--entry ] [--out-dir ] [--out-file ] [--shim-out ] [--target js/wasm] [--optimize size|speed] [--tinygo] [--no-shim] [--clean] [--json] @@ -326,6 +355,8 @@ The build writes: - `dist/worker.mjs` - `dist/wasm_exec.js` +For JavaScript projects, `build` validates that the configured Worker entrypoint exists and returns it without compiling. `build wasm` remains the explicit Go-only subcommand. + ### `dev` Start a Wrangler-powered development session. @@ -338,7 +369,7 @@ Flags `--open` and `--watch` exist as reserved compatibility flags. ### `deploy` -Validate, build, and deploy the Worker. +Validate, build if needed, and deploy the Worker. ```bash ./flare-edge-cli deploy [--path ] [--env ] [--name ] [--compat-date ] [--route ] [--custom-domain ] [--workers-dev] [--dry-run] [--upload-only] [--message ] [--var ] [--keep-vars] [--minify] [--latest] [--json] @@ -346,8 +377,9 @@ Validate, build, and deploy the Worker. Behavior: -- runs compatibility checks first -- builds the Wasm artifact and Worker shim +- runs compatibility checks first for Go/Wasm projects +- builds the Wasm artifact and Worker shim for Go/Wasm projects +- stages the configured main module for JavaScript projects - updates Wrangler config - deploys through Wrangler - can attach routes and custom domains during deploy @@ -439,12 +471,12 @@ Check whether the local environment and project are deployable. `doctor` checks: -- Go installation +- Go installation when the project runtime requires it - Wrangler installation - auth health - project config validity - compatibility date presence -- Wasm build readiness +- Wasm build readiness for Go projects or JavaScript entrypoint presence for JS projects - binding/config sanity ### `teardown` @@ -477,7 +509,7 @@ flare-edge-cli tail -> flare-edge-cli logs tail flare-edge-cli rollback -> flare-edge-cli release rollback ``` -The top-level `build` command is itself the primary Wasm build entrypoint, with `build wasm` available as an explicit subcommand. +The top-level `build` command is runtime-aware, with `build wasm` available as the explicit Go/Wasm subcommand. ## Examples @@ -487,6 +519,18 @@ Scaffold a JSON worker: ./flare-edge-cli init test-project --module-path github.com/example/test-project --template edge-json ``` +Scaffold a native JavaScript Worker: + +```bash +./flare-edge-cli init js-worker --runtime js +``` + +Scaffold a JavaScript Worker with Node.js compatibility enabled: + +```bash +./flare-edge-cli init js-worker --runtime js --node-compat +``` + Scaffold a Go-based AI Worker: ```bash diff --git a/app_structure_llm.txt b/app_structure_llm.txt index 9e16b39..4683856 100644 --- a/app_structure_llm.txt +++ b/app_structure_llm.txt @@ -1,5 +1,5 @@ # App Structure - flare-edge-cli -# Last updated: 2026-03-19 +# Last updated: 2026-03-25 ## Repositories - GitHub: https://github.com/Gladium-AI/flare-edge-cli @@ -15,6 +15,6 @@ - CLOUDFLARE_ACCOUNT_ID (optional, for account selection) ## Notes -- Local-first CLI project for Cloudflare Workers Go/Wasm workflows. +- Local-first CLI project for Cloudflare Workers Go/Wasm and native JavaScript workflows. - Uses Wrangler for deployment and resource operations. - GitHub automation includes CI quality gates, security scanning, and Dependabot updates under `.github/`. diff --git a/internal/cli/aliases.go b/internal/cli/aliases.go index 0e1096b..2b85c16 100644 --- a/internal/cli/aliases.go +++ b/internal/cli/aliases.go @@ -28,10 +28,12 @@ func newInitAliasCommand(deps Dependencies) *cobra.Command { }, } cmd.Flags().StringVar(&options.Dir, "cwd", ".", "Target parent directory") - cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Go module path") + cmd.Flags().StringVar(&options.Runtime, "runtime", "go", "Project runtime: go|js") + cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Module path") cmd.Flags().StringVar(&options.PackageName, "package", "", "Package name") - cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings") + cmd.Flags().StringVar(&options.Template, "template", "", "Starter template. Go: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings. JS: js-worker") cmd.Flags().StringVar(&options.CompatDate, "compat-date", "", "Compatibility date") + cmd.Flags().BoolVar(&options.NodeCompat, "node-compat", false, "Enable Cloudflare's nodejs_compat flag for JavaScript workers") cmd.Flags().StringVar(&options.Env, "env", "", "Wrangler environment") cmd.Flags().BoolVar(&options.UseJSONC, "use-jsonc", false, "Generate wrangler.jsonc") cmd.Flags().BoolVar(&options.WithGit, "with-git", true, "Generate .gitignore") @@ -79,7 +81,7 @@ func newCheckAliasCommand(deps Dependencies) *cobra.Command { } cmd.Flags().StringVar(&options.Path, "path", ".", "Project path") cmd.Flags().StringVar(&options.Entry, "entry", "", "Package or file entry selector") - cmd.Flags().StringVar(&options.Profile, "profile", "worker-wasm", "Profile name") + cmd.Flags().StringVar(&options.Profile, "profile", "", "Profile name") cmd.Flags().StringVar(&options.FailOn, "fail-on", "error", "Threshold") cmd.Flags().StringArrayVar(&options.Exclude, "exclude", nil, "Exclude globs") cmd.Flags().BoolVar(&jsonOutput, "json", false, "Emit machine-readable JSON") diff --git a/internal/cli/build.go b/internal/cli/build.go index a2923d5..30711d3 100644 --- a/internal/cli/build.go +++ b/internal/cli/build.go @@ -15,10 +15,10 @@ func newBuildCommand(deps Dependencies) *cobra.Command { cmd := &cobra.Command{ Use: "build", - Short: "Build Wasm artifacts for Workers", + Short: "Build or stage Worker artifacts", Aliases: []string{"compile"}, RunE: func(cmd *cobra.Command, _ []string) error { - result, err := deps.Services.Build.Wasm(context.Background(), options) + result, err := deps.Services.Build.Build(context.Background(), options) if err != nil { return err } diff --git a/internal/cli/compat.go b/internal/cli/compat.go index fcb034d..1afc8fc 100644 --- a/internal/cli/compat.go +++ b/internal/cli/compat.go @@ -52,7 +52,7 @@ func newCompatCheckCommand(deps Dependencies) *cobra.Command { cmd.Flags().StringVar(&options.Path, "path", ".", "Project path to analyze") cmd.Flags().StringVar(&options.Entry, "entry", "", "Package or file entry selector") - cmd.Flags().StringVar(&options.Profile, "profile", "worker-wasm", "Compatibility rule profile") + cmd.Flags().StringVar(&options.Profile, "profile", "", "Compatibility rule profile") cmd.Flags().BoolVar(&options.Strict, "strict", false, "Enable stricter compatibility checks") cmd.Flags().BoolVar(&jsonOutput, "json", false, "Emit machine-readable JSON") cmd.Flags().BoolVar(&sarif, "sarif", false, "Emit SARIF JSON") diff --git a/internal/cli/project.go b/internal/cli/project.go index 706d56c..785bfd6 100644 --- a/internal/cli/project.go +++ b/internal/cli/project.go @@ -26,7 +26,7 @@ func newProjectInitCommand(deps Dependencies) *cobra.Command { cmd := &cobra.Command{ Use: "init ", - Short: "Scaffold a Workers-ready Go/Wasm project", + Short: "Scaffold a Workers-ready project", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.Name = args[0] @@ -39,10 +39,12 @@ func newProjectInitCommand(deps Dependencies) *cobra.Command { }, } - cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Go module path to initialize") + cmd.Flags().StringVar(&options.Runtime, "runtime", "go", "Project runtime: go|js") + cmd.Flags().StringVar(&options.ModulePath, "module-path", "", "Module path to initialize") cmd.Flags().StringVar(&options.PackageName, "package", "", "Package name for generated Go entrypoint") - cmd.Flags().StringVar(&options.Template, "template", "edge-http", "Starter template: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings") + cmd.Flags().StringVar(&options.Template, "template", "", "Starter template. Go: edge-http|edge-json|scheduled|kv-api|d1-api|r2-api|ai-text|ai-chat|ai-vision|ai-stt|ai-tts|ai-image|ai-embeddings. JS: js-worker") cmd.Flags().StringVar(&options.CompatDate, "compat-date", "", "Cloudflare compatibility date") + cmd.Flags().BoolVar(&options.NodeCompat, "node-compat", false, "Enable Cloudflare's nodejs_compat flag for JavaScript workers") cmd.Flags().StringVar(&options.Env, "env", "", "Default Wrangler environment") cmd.Flags().BoolVar(&options.UseJSONC, "use-jsonc", false, "Generate wrangler.jsonc output") cmd.Flags().BoolVar(&options.WithGit, "with-git", true, "Generate .gitignore in the scaffolded project") diff --git a/internal/cli/root.go b/internal/cli/root.go index d85660c..5c5247e 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -11,7 +11,7 @@ func NewRootCommand(deps Dependencies) *cobra.Command { cmd := &cobra.Command{ Use: "flare-edge-cli", - Short: "Build and deploy Go/Wasm Workers with Cloudflare", + Short: "Build and deploy Cloudflare Workers", SilenceUsage: true, SilenceErrors: true, PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { diff --git a/internal/domain/config/project.go b/internal/domain/config/project.go index 8cc70c8..d507bc0 100644 --- a/internal/domain/config/project.go +++ b/internal/domain/config/project.go @@ -1,28 +1,38 @@ package config -import "fmt" +import ( + "fmt" + "path/filepath" +) const ( - DefaultProjectConfigFile = "flare-edge.json" - DefaultWranglerConfigFile = "wrangler.jsonc" - DefaultCompatibilityDate = "2026-03-19" - DefaultCompatibilityProfile = "worker-wasm" + DefaultProjectConfigFile = "flare-edge.json" + DefaultWranglerConfigFile = "wrangler.jsonc" + DefaultCompatibilityDate = "2026-03-19" + DefaultCompatibilityProfile = "worker-wasm" + DefaultJSCompatibilityProfile = "worker-js" + + RuntimeGoWasm = "go-wasm" + RuntimeJavaScript = "js-worker" ) type Project struct { SchemaVersion int `json:"schema_version" validate:"required,min=1"` ProjectName string `json:"project_name" validate:"required"` + Runtime string `json:"runtime,omitempty" validate:"omitempty,oneof=go-wasm js-worker"` ModulePath string `json:"module_path" validate:"required"` PackageName string `json:"package_name" validate:"required"` Template string `json:"template" validate:"required"` Entry string `json:"entry" validate:"required"` + Main string `json:"main,omitempty"` OutDir string `json:"out_dir" validate:"required"` - WasmFile string `json:"wasm_file" validate:"required"` - ShimFile string `json:"shim_file" validate:"required"` + WasmFile string `json:"wasm_file,omitempty"` + ShimFile string `json:"shim_file,omitempty"` WorkerName string `json:"worker_name" validate:"required"` WranglerConfig string `json:"wrangler_config" validate:"required"` CompatibilityDate string `json:"compatibility_date" validate:"required"` CompatibilityProfile string `json:"compatibility_profile" validate:"required"` + NodeCompat bool `json:"nodejs_compat,omitempty"` Env string `json:"env,omitempty"` Bindings ProjectBindings `json:"bindings"` Environments map[string]Environment `json:"environments,omitempty"` @@ -39,7 +49,7 @@ type ProjectBindings struct { } type GeneratedArtifacts struct { - ShimSource string `json:"shim_source" validate:"required"` + ShimSource string `json:"shim_source,omitempty"` WasmExecSource string `json:"wasm_exec_source,omitempty"` } @@ -75,32 +85,46 @@ type R2Binding struct { } func DefaultProject(name, modulePath, packageName, template, compatDate, env string) Project { + return DefaultProjectWithRuntime(name, modulePath, packageName, template, compatDate, env, RuntimeGoWasm, false) +} + +func DefaultProjectWithRuntime(name, modulePath, packageName, template, compatDate, env, runtime string, nodeCompat bool) Project { if compatDate == "" { compatDate = DefaultCompatibilityDate } - entry := "./cmd/worker" project := Project{ - SchemaVersion: 1, - ProjectName: name, - ModulePath: modulePath, - PackageName: packageName, - Template: template, - Entry: entry, - OutDir: "dist", - WasmFile: "app.wasm", - ShimFile: "worker.mjs", - WorkerName: name, - WranglerConfig: DefaultWranglerConfigFile, - CompatibilityDate: compatDate, - CompatibilityProfile: DefaultCompatibilityProfile, - Env: env, - Bindings: ProjectBindings{Vars: map[string]string{}}, - Environments: map[string]Environment{}, - Generated: GeneratedArtifacts{ + SchemaVersion: 1, + ProjectName: name, + Runtime: normalizeRuntime(runtime), + ModulePath: modulePath, + PackageName: packageName, + Template: template, + WorkerName: name, + WranglerConfig: DefaultWranglerConfigFile, + CompatibilityDate: compatDate, + NodeCompat: nodeCompat, + Env: env, + Bindings: ProjectBindings{Vars: map[string]string{}}, + Environments: map[string]Environment{}, + } + switch project.Runtime { + case RuntimeJavaScript: + project.Entry = filepath.ToSlash(filepath.Join("src", "worker.mjs")) + project.Main = project.Entry + project.OutDir = "src" + project.CompatibilityProfile = DefaultJSCompatibilityProfile + default: + project.Entry = "./cmd/worker" + project.OutDir = "dist" + project.WasmFile = "app.wasm" + project.ShimFile = "worker.mjs" + project.Main = filepath.ToSlash(filepath.Join(project.OutDir, project.ShimFile)) + project.CompatibilityProfile = DefaultCompatibilityProfile + project.Generated = GeneratedArtifacts{ ShimSource: "internal/generated/worker_shim.mjs", WasmExecSource: "internal/generated/wasm_exec.js", - }, + } } if UsesAIBinding(template) { project.Bindings.AI = &AIBinding{Binding: "AI", Remote: true} @@ -109,9 +133,30 @@ func DefaultProject(name, modulePath, packageName, template, compatDate, env str } func (p Project) ArtifactPath() string { + if p.WasmFile == "" { + return "" + } return fmt.Sprintf("%s/%s", p.OutDir, p.WasmFile) } +func (p Project) EffectiveRuntime() string { + return normalizeRuntime(p.Runtime) +} + +func (p Project) MainPath() string { + if p.Main != "" { + return filepath.ToSlash(p.Main) + } + if p.ShimFile != "" { + return filepath.ToSlash(filepath.Join(p.OutDir, p.ShimFile)) + } + return filepath.ToSlash(p.Entry) +} + +func (p Project) RequiresBuild() bool { + return p.EffectiveRuntime() == RuntimeGoWasm +} + func UsesAIBinding(template string) bool { switch template { case "ai-text", "ai-chat", "ai-vision", "ai-stt", "ai-tts", "ai-image", "ai-embeddings": @@ -120,3 +165,12 @@ func UsesAIBinding(template string) bool { return false } } + +func normalizeRuntime(runtime string) string { + switch runtime { + case RuntimeJavaScript: + return RuntimeJavaScript + default: + return RuntimeGoWasm + } +} diff --git a/internal/domain/config/wrangler.go b/internal/domain/config/wrangler.go index d7fe5ea..98a5f54 100644 --- a/internal/domain/config/wrangler.go +++ b/internal/domain/config/wrangler.go @@ -6,18 +6,19 @@ import ( ) type WranglerConfig struct { - Name string `json:"name"` - Main string `json:"main"` - CompatibilityDate string `json:"compatibility_date"` - Observability *WranglerObservability `json:"observability,omitempty"` - AI *WranglerAIBinding `json:"ai,omitempty"` - Vars map[string]string `json:"vars,omitempty"` - KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"` - D1Databases []WranglerD1Database `json:"d1_databases,omitempty"` - R2Buckets []WranglerR2Bucket `json:"r2_buckets,omitempty"` - Routes []WranglerRoute `json:"routes,omitempty"` - Env map[string]WranglerEnvConfig `json:"env,omitempty"` - Extra map[string]json.RawMessage `json:"-"` + Name string `json:"name"` + Main string `json:"main"` + CompatibilityDate string `json:"compatibility_date"` + CompatibilityFlags []string `json:"compatibility_flags,omitempty"` + Observability *WranglerObservability `json:"observability,omitempty"` + AI *WranglerAIBinding `json:"ai,omitempty"` + Vars map[string]string `json:"vars,omitempty"` + KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"` + D1Databases []WranglerD1Database `json:"d1_databases,omitempty"` + R2Buckets []WranglerR2Bucket `json:"r2_buckets,omitempty"` + Routes []WranglerRoute `json:"routes,omitempty"` + Env map[string]WranglerEnvConfig `json:"env,omitempty"` + Extra map[string]json.RawMessage `json:"-"` } type WranglerObservability struct { @@ -25,14 +26,15 @@ type WranglerObservability struct { } type WranglerEnvConfig struct { - Name string `json:"name,omitempty"` - AI *WranglerAIBinding `json:"ai,omitempty"` - Vars map[string]string `json:"vars,omitempty"` - KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"` - D1Databases []WranglerD1Database `json:"d1_databases,omitempty"` - R2Buckets []WranglerR2Bucket `json:"r2_buckets,omitempty"` - Routes []WranglerRoute `json:"routes,omitempty"` - Extra map[string]json.RawMessage `json:"-"` + Name string `json:"name,omitempty"` + AI *WranglerAIBinding `json:"ai,omitempty"` + Vars map[string]string `json:"vars,omitempty"` + CompatibilityFlags []string `json:"compatibility_flags,omitempty"` + KVNamespaces []WranglerKVNamespace `json:"kv_namespaces,omitempty"` + D1Databases []WranglerD1Database `json:"d1_databases,omitempty"` + R2Buckets []WranglerR2Bucket `json:"r2_buckets,omitempty"` + Routes []WranglerRoute `json:"routes,omitempty"` + Extra map[string]json.RawMessage `json:"-"` } type WranglerAIBinding struct { @@ -94,6 +96,7 @@ func (w WranglerConfig) MarshalJSON() ([]byte, error) { putString(raw, "name", w.Name) putString(raw, "main", w.Main) putValue(raw, "compatibility_date", w.CompatibilityDate) + putValue(raw, "compatibility_flags", w.CompatibilityFlags) putValue(raw, "observability", w.Observability) putValue(raw, "ai", w.AI) putValue(raw, "vars", w.Vars) @@ -135,6 +138,7 @@ func (w WranglerEnvConfig) MarshalJSON() ([]byte, error) { putString(raw, "name", w.Name) putValue(raw, "ai", w.AI) putValue(raw, "vars", w.Vars) + putValue(raw, "compatibility_flags", w.CompatibilityFlags) putValue(raw, "kv_namespaces", w.KVNamespaces) putValue(raw, "d1_databases", w.D1Databases) putValue(raw, "r2_buckets", w.R2Buckets) @@ -158,9 +162,11 @@ func deleteKnownWranglerKeys(raw map[string]json.RawMessage) { delete(raw, "name") delete(raw, "main") delete(raw, "compatibility_date") + delete(raw, "compatibility_flags") delete(raw, "observability") delete(raw, "ai") delete(raw, "vars") + delete(raw, "compatibility_flags") delete(raw, "kv_namespaces") delete(raw, "d1_databases") delete(raw, "r2_buckets") @@ -217,6 +223,8 @@ func isEmptyJSONValue(value any) bool { return len(typed) == 0 case map[string]WranglerEnvConfig: return len(typed) == 0 + case []string: + return len(typed) == 0 case []WranglerKVNamespace: return len(typed) == 0 case []WranglerD1Database: diff --git a/internal/domain/config/wrangler_test.go b/internal/domain/config/wrangler_test.go index 8d37fa7..67ff75f 100644 --- a/internal/domain/config/wrangler_test.go +++ b/internal/domain/config/wrangler_test.go @@ -8,14 +8,16 @@ import ( func TestWranglerConfigRoundTripPreservesUnknownFields(t *testing.T) { input := []byte(`{ - "name": "demo-worker", + "name": "demo-worker", "main": "dist/worker.mjs", "compatibility_date": "2026-03-19", + "compatibility_flags": ["nodejs_compat"], "ai": { "binding": "AI", "remote": true }, "unsafe": { "bindings": [{ "name": "EXTRA" }] }, "env": { "production": { "vars": { "FOO": "bar" }, + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }] } @@ -42,6 +44,9 @@ func TestWranglerConfigRoundTripPreservesUnknownFields(t *testing.T) { if !strings.Contains(text, `"durable_objects":{"bindings":[{"name":"COUNTER","class_name":"Counter"}]}`) { t.Fatalf("unknown env field was not preserved: %s", text) } + if !strings.Contains(text, `"compatibility_flags":["nodejs_compat"]`) { + t.Fatalf("known compatibility flags were not preserved: %s", text) + } if !strings.Contains(text, `"vars":{"BAR":"baz"}`) { t.Fatalf("known var update was not written: %s", text) } diff --git a/internal/service/build/service.go b/internal/service/build/service.go index 1d76262..c889727 100644 --- a/internal/service/build/service.go +++ b/internal/service/build/service.go @@ -37,6 +37,9 @@ type WasmOptions struct { } type WasmResult struct { + Runtime string `json:"runtime"` + Built bool `json:"built"` + Main string `json:"main"` Artifact string `json:"artifact"` Shim string `json:"shim,omitempty"` Files []string `json:"files"` @@ -66,12 +69,29 @@ func NewService(store *configstore.Store, fs *fs.FileSystem, runner process.Runn } } +func (s *Service) Build(ctx context.Context, options WasmOptions) (WasmResult, error) { + project, err := s.loadProject(options.Path) + if err != nil { + return WasmResult{}, err + } + if !project.RequiresBuild() { + return s.stageJavaScript(options.Path, project) + } + return s.buildWasm(ctx, project, options) +} + func (s *Service) Wasm(ctx context.Context, options WasmOptions) (WasmResult, error) { project, err := s.loadProject(options.Path) if err != nil { return WasmResult{}, err } + if !project.RequiresBuild() { + return WasmResult{}, fmt.Errorf("runtime %q does not produce Wasm artifacts", project.EffectiveRuntime()) + } + return s.buildWasm(ctx, project, options) +} +func (s *Service) buildWasm(ctx context.Context, project config.Project, options WasmOptions) (WasmResult, error) { outDir := defaultString(options.OutDir, filepath.Join(options.Path, project.OutDir)) outFile := defaultString(options.OutFile, project.WasmFile) shimOut := defaultString(options.ShimOut, filepath.Join(outDir, project.ShimFile)) @@ -124,6 +144,9 @@ func (s *Service) Wasm(ctx context.Context, options WasmOptions) (WasmResult, er } return WasmResult{ + Runtime: project.EffectiveRuntime(), + Built: true, + Main: filepath.Join(options.Path, project.MainPath()), Artifact: artifact, Shim: shimOut, Files: files, @@ -131,6 +154,23 @@ func (s *Service) Wasm(ctx context.Context, options WasmOptions) (WasmResult, er }, nil } +func (s *Service) stageJavaScript(dir string, project config.Project) (WasmResult, error) { + mainPath := filepath.Join(dir, project.MainPath()) + exists, err := s.fs.Exists(mainPath) + if err != nil { + return WasmResult{}, err + } + if !exists { + return WasmResult{}, fmt.Errorf("worker entrypoint is missing: %s", mainPath) + } + return WasmResult{ + Runtime: project.EffectiveRuntime(), + Built: false, + Main: mainPath, + Files: []string{mainPath}, + }, nil +} + func (s *Service) Inspect(_ context.Context, options InspectOptions) (InspectResult, error) { data, err := s.fs.ReadFile(options.Artifact) if err != nil { diff --git a/internal/service/build/service_test.go b/internal/service/build/service_test.go index d170437..af15e79 100644 --- a/internal/service/build/service_test.go +++ b/internal/service/build/service_test.go @@ -97,3 +97,51 @@ func TestWasmUsesStableArtifactPathAndRequiresArtifact(t *testing.T) { t.Fatalf("expected artifact to exist at %s", expectedArtifact) } } + +func TestBuildStagesJavaScriptWorkerWithoutCompiling(t *testing.T) { + tmpDir := t.TempDir() + projectDir := filepath.Join(tmpDir, "services", "js-worker") + filesystem := fs.New() + store := configstore.New(filesystem) + + project := config.DefaultProjectWithRuntime( + "js-worker", + "github.com/paolo/js-worker", + "main", + "js-worker", + config.DefaultCompatibilityDate, + "", + config.RuntimeJavaScript, + true, + ) + if err := store.SaveProject(projectDir, project); err != nil { + t.Fatalf("save project: %v", err) + } + if err := os.MkdirAll(filepath.Join(projectDir, "src"), 0o755); err != nil { + t.Fatalf("mkdir src: %v", err) + } + if err := os.WriteFile(filepath.Join(projectDir, "src", "worker.mjs"), []byte("export default {};"), 0o644); err != nil { + t.Fatalf("write worker: %v", err) + } + + runner := &fakeRunner{} + service := NewService(store, filesystem, runner, &toolchain.GoToolchain{}) + + result, err := service.Build(context.Background(), WasmOptions{Path: projectDir}) + if err != nil { + t.Fatalf("build js worker: %v", err) + } + if result.Built { + t.Fatalf("expected JavaScript worker build to be skipped") + } + if result.Runtime != config.RuntimeJavaScript { + t.Fatalf("unexpected runtime: %q", result.Runtime) + } + expectedMain := filepath.Join(projectDir, "src", "worker.mjs") + if result.Main != expectedMain { + t.Fatalf("unexpected main path: %q", result.Main) + } + if len(runner.last.Args) != 0 { + t.Fatalf("expected no compiler invocation, got %v", runner.last.Args) + } +} diff --git a/internal/service/compat/service.go b/internal/service/compat/service.go index 135fe67..dcd254c 100644 --- a/internal/service/compat/service.go +++ b/internal/service/compat/service.go @@ -2,15 +2,18 @@ package compat import ( "context" + "encoding/json" "fmt" "go/ast" "go/token" + "os" "path/filepath" "sort" "strconv" "strings" domaincompat "github.com/paolo/flare-edge-cli/internal/domain/compat" + "github.com/paolo/flare-edge-cli/internal/domain/config" "github.com/paolo/flare-edge-cli/internal/domain/diagnostic" "golang.org/x/tools/go/packages" ) @@ -39,6 +42,16 @@ func NewService() *Service { } func (s *Service) Check(_ context.Context, options CheckOptions) (CheckResult, error) { + if project, ok, err := loadProject(options.Path); err != nil { + return CheckResult{}, err + } else if ok && !project.RequiresBuild() { + return CheckResult{ + Profile: defaultProjectProfile(project, options.Profile), + FailOn: defaultString(options.FailOn, "error"), + Diagnostics: nil, + }, nil + } + loadMode := packages.NeedName | packages.NeedCompiledGoFiles | packages.NeedSyntax cfg := &packages.Config{ Mode: loadMode, @@ -107,6 +120,36 @@ func (s *Service) Check(_ context.Context, options CheckOptions) (CheckResult, e return result, nil } +func loadProject(dir string) (config.Project, bool, error) { + if dir == "" { + dir = "." + } + path := filepath.Join(dir, config.DefaultProjectConfigFile) + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return config.Project{}, false, nil + } + return config.Project{}, false, fmt.Errorf("read %s: %w", path, err) + } + + var project config.Project + if err := json.Unmarshal(data, &project); err != nil { + return config.Project{}, false, fmt.Errorf("decode %s: %w", path, err) + } + return project, true, nil +} + +func defaultProjectProfile(project config.Project, profile string) string { + if profile != "" { + return profile + } + if project.CompatibilityProfile != "" { + return project.CompatibilityProfile + } + return defaultString(profile, "worker-wasm") +} + func (s *Service) Rules(severity string) []domaincompat.Rule { all := domaincompat.BuiltInRules() if severity == "" { diff --git a/internal/service/compat/service_test.go b/internal/service/compat/service_test.go index 9e44361..19087d0 100644 --- a/internal/service/compat/service_test.go +++ b/internal/service/compat/service_test.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/paolo/flare-edge-cli/internal/domain/config" ) func TestCheckFindsUnsupportedPatterns(t *testing.T) { @@ -40,3 +42,39 @@ func main() { } } } + +func TestCheckSkipsStaticAnalysisForJavaScriptWorkers(t *testing.T) { + dir := t.TempDir() + project := []byte(`{ + "schema_version": 1, + "project_name": "js-worker", + "runtime": "js-worker", + "module_path": "github.com/example/js-worker", + "package_name": "main", + "template": "js-worker", + "entry": "src/worker.mjs", + "main": "src/worker.mjs", + "out_dir": "src", + "worker_name": "js-worker", + "wrangler_config": "wrangler.jsonc", + "compatibility_date": "2026-03-19", + "compatibility_profile": "worker-js", + "nodejs_compat": true, + "bindings": {}, + "generated": {} +}`) + if err := os.WriteFile(filepath.Join(dir, config.DefaultProjectConfigFile), project, 0o644); err != nil { + t.Fatal(err) + } + + result, err := NewService().Check(context.Background(), CheckOptions{Path: dir}) + if err != nil { + t.Fatal(err) + } + if result.Profile != config.DefaultJSCompatibilityProfile { + t.Fatalf("unexpected profile: %q", result.Profile) + } + if result.ErrorCount != 0 || result.WarnCount != 0 || len(result.Diagnostics) != 0 { + t.Fatalf("expected JavaScript worker compatibility check to be skipped, got %+v", result) + } +} diff --git a/internal/service/deploy/service.go b/internal/service/deploy/service.go index d70ff76..dabd844 100644 --- a/internal/service/deploy/service.go +++ b/internal/service/deploy/service.go @@ -50,20 +50,30 @@ func NewService(store *configstore.Store, fs *fs.FileSystem, build *buildsvc.Ser } func (s *Service) Deploy(ctx context.Context, options Options) (Result, error) { - compatibility, err := s.compat.Check(ctx, compatsvc.CheckOptions{Path: options.Dir, Profile: "worker-wasm", FailOn: "error"}) + project, wranglerCfg, err := shared.LoadProjectAndWrangler(options.Dir, s.store, s.fs) if err != nil { return Result{}, err } - if compatibility.ErrorCount > 0 { - return Result{}, fmt.Errorf("compatibility check failed with %d error(s)", compatibility.ErrorCount) - } - buildResult, err := s.build.Wasm(ctx, buildsvc.WasmOptions{Path: options.Dir}) - if err != nil { - return Result{}, err + compatibility := compatsvc.CheckResult{ + Profile: project.CompatibilityProfile, + FailOn: "error", + } + if project.RequiresBuild() { + compatibility, err = s.compat.Check(ctx, compatsvc.CheckOptions{ + Path: options.Dir, + Profile: project.CompatibilityProfile, + FailOn: "error", + }) + if err != nil { + return Result{}, err + } + if compatibility.ErrorCount > 0 { + return Result{}, fmt.Errorf("compatibility check failed with %d error(s)", compatibility.ErrorCount) + } } - project, wranglerCfg, err := shared.LoadProjectAndWrangler(options.Dir, s.store, s.fs) + buildResult, err := s.build.Build(ctx, buildsvc.WasmOptions{Path: options.Dir}) if err != nil { return Result{}, err } diff --git a/internal/service/doctor/service.go b/internal/service/doctor/service.go index 26b711f..156b43f 100644 --- a/internal/service/doctor/service.go +++ b/internal/service/doctor/service.go @@ -46,12 +46,6 @@ func NewService(store *configstore.Store, fs *fs.FileSystem, runner process.Runn func (s *Service) Run(ctx context.Context, options Options) (Result, error) { var checks []Check - if version, err := s.goTool.Version(ctx, options.Dir); err == nil { - checks = append(checks, Check{Name: "go", Status: "ok", Details: version}) - } else { - checks = append(checks, Check{Name: "go", Status: "error", Details: err.Error()}) - } - if result, err := s.runner.Run(ctx, process.Command{Name: "wrangler", Args: []string{"--version"}, Dir: options.Dir}); err == nil { checks = append(checks, Check{Name: "wrangler", Status: "ok", Details: result.Stdout}) } else { @@ -76,6 +70,7 @@ func (s *Service) Run(ctx context.Context, options Options) (Result, error) { checks = append(checks, Check{Name: "project-config", Status: "error", Details: err.Error()}) } else { checks = append(checks, Check{Name: "project-config", Status: "ok", Details: project.WorkerName}) + checks = append(checks, Check{Name: "runtime", Status: "ok", Details: project.EffectiveRuntime()}) if project.CompatibilityDate == "" { checks = append(checks, Check{Name: "compatibility-date", Status: "error", Details: "compatibility date missing"}) } else { @@ -88,14 +83,33 @@ func (s *Service) Run(ctx context.Context, options Options) (Result, error) { checks = append(checks, Check{Name: "ai-binding", Status: "warning", Details: "Workers AI should use remote=true for development"}) } } - if _, err := s.build.Wasm(ctx, buildsvc.WasmOptions{Path: options.Dir, NoShim: true, OutDir: filepath.Join(options.Dir, ".doctor-dist"), Clean: true}); err != nil { - checks = append(checks, Check{Name: "wasm-build", Status: "error", Details: err.Error()}) + if project.RequiresBuild() { + if version, err := s.goTool.Version(ctx, options.Dir); err == nil { + checks = append(checks, Check{Name: "go", Status: "ok", Details: version}) + } else { + checks = append(checks, Check{Name: "go", Status: "error", Details: err.Error()}) + } + if _, err := s.build.Wasm(ctx, buildsvc.WasmOptions{Path: options.Dir, NoShim: true, OutDir: filepath.Join(options.Dir, ".doctor-dist"), Clean: true}); err != nil { + checks = append(checks, Check{Name: "wasm-build", Status: "error", Details: err.Error()}) + } else { + checks = append(checks, Check{Name: "wasm-build", Status: "ok", Details: "Wasm target buildable"}) + } } else { - checks = append(checks, Check{Name: "wasm-build", Status: "ok", Details: "Wasm target buildable"}) + checks = append(checks, Check{Name: "go", Status: "skipped", Details: "not required for js-worker runtime"}) + if _, err := s.build.Build(ctx, buildsvc.WasmOptions{Path: options.Dir}); err != nil { + checks = append(checks, Check{Name: "worker-entry", Status: "error", Details: err.Error()}) + } else { + checks = append(checks, Check{Name: "worker-entry", Status: "ok", Details: "JavaScript worker entrypoint present"}) + } } } } else { checks = append(checks, Check{Name: "project-config", Status: "warning", Details: "flare-edge.json not found"}) + if version, err := s.goTool.Version(ctx, options.Dir); err == nil { + checks = append(checks, Check{Name: "go", Status: "ok", Details: version}) + } else { + checks = append(checks, Check{Name: "go", Status: "error", Details: err.Error()}) + } } return Result{Checks: checks}, nil diff --git a/internal/service/project/service.go b/internal/service/project/service.go index 4d70be8..98d34fb 100644 --- a/internal/service/project/service.go +++ b/internal/service/project/service.go @@ -19,10 +19,12 @@ type Service struct { type InitOptions struct { Dir string Name string + Runtime string ModulePath string PackageName string Template string CompatDate string + NodeCompat bool Env string UseJSONC bool WithGit bool @@ -43,10 +45,13 @@ type InfoOptions struct { type InfoResult struct { Name string `json:"name"` + Runtime string `json:"runtime"` Entrypoint string `json:"entrypoint"` + Main string `json:"main"` WorkerName string `json:"worker_name"` CompatibilityDate string `json:"compatibility_date"` CompatibilityMode string `json:"compatibility_profile"` + NodeCompat bool `json:"nodejs_compat,omitempty"` OutputDir string `json:"output_dir"` WasmFile string `json:"wasm_file"` ShimFile string `json:"shim_file"` @@ -72,21 +77,40 @@ func (s *Service) Init(_ context.Context, options InitOptions) (InitResult, erro return InitResult{}, fmt.Errorf("project directory already exists: %s", projectDir) } + runtime := normalizeRuntime(options.Runtime) + template, err := resolveTemplate(runtime, options.Template) + if err != nil { + return InitResult{}, err + } + project := config.DefaultProject( options.Name, defaultString(options.ModulePath, "github.com/paolo/"+options.Name), defaultString(options.PackageName, "main"), - defaultString(options.Template, "edge-http"), + template, options.CompatDate, options.Env, ) + project = config.DefaultProjectWithRuntime( + options.Name, + project.ModulePath, + project.PackageName, + template, + options.CompatDate, + options.Env, + runtime, + options.NodeCompat, + ) wrangler := config.WranglerConfig{ Name: project.WorkerName, - Main: filepath.ToSlash(filepath.Join(project.OutDir, project.ShimFile)), + Main: project.MainPath(), CompatibilityDate: project.CompatibilityDate, Observability: &config.WranglerObservability{Enabled: true}, } + if project.NodeCompat { + wrangler.CompatibilityFlags = append(wrangler.CompatibilityFlags, "nodejs_compat") + } if project.Bindings.AI != nil { wrangler.AI = &config.WranglerAIBinding{ Binding: project.Bindings.AI.Binding, @@ -129,10 +153,13 @@ func (s *Service) Info(_ context.Context, options InfoOptions) (InfoResult, erro result := InfoResult{ Name: project.ProjectName, + Runtime: project.EffectiveRuntime(), Entrypoint: project.Entry, + Main: project.MainPath(), WorkerName: project.WorkerName, CompatibilityDate: project.CompatibilityDate, CompatibilityMode: project.CompatibilityProfile, + NodeCompat: project.NodeCompat, OutputDir: project.OutDir, WasmFile: project.WasmFile, ShimFile: project.ShimFile, @@ -154,3 +181,36 @@ func defaultString(value, fallback string) string { } return value } + +func normalizeRuntime(value string) string { + switch strings.TrimSpace(strings.ToLower(value)) { + case "", "go", "go-wasm", "wasm": + return config.RuntimeGoWasm + case "js", "javascript", "js-worker", "node", "nodejs": + return config.RuntimeJavaScript + default: + return value + } +} + +func resolveTemplate(runtime, template string) (string, error) { + selected := strings.TrimSpace(template) + if selected == "" { + if runtime == config.RuntimeJavaScript { + return "js-worker", nil + } + return "edge-http", nil + } + + if runtime == config.RuntimeJavaScript { + if selected != "js-worker" { + return "", fmt.Errorf("runtime %q only supports template %q", runtime, "js-worker") + } + return selected, nil + } + + if selected == "js-worker" { + return "", fmt.Errorf("template %q requires --runtime js", selected) + } + return selected, nil +} diff --git a/internal/service/project/service_test.go b/internal/service/project/service_test.go index 86edd5c..1aac273 100644 --- a/internal/service/project/service_test.go +++ b/internal/service/project/service_test.go @@ -83,3 +83,60 @@ func TestInitAITemplate(t *testing.T) { }) } } + +func TestInitJavaScriptTemplateWithNodeCompat(t *testing.T) { + t.Parallel() + + filesystem := fs.New() + store := configstore.New(filesystem) + service := NewService(store, filesystem) + root := t.TempDir() + + result, err := service.Init(context.Background(), InitOptions{ + Dir: root, + Name: "js-worker", + Runtime: "js", + NodeCompat: true, + WithGit: true, + }) + if err != nil { + t.Fatalf("init project: %v", err) + } + + project, err := store.LoadProject(result.ProjectDir) + if err != nil { + t.Fatalf("load project: %v", err) + } + if project.EffectiveRuntime() != "js-worker" { + t.Fatalf("unexpected runtime: %q", project.EffectiveRuntime()) + } + if project.Template != "js-worker" { + t.Fatalf("unexpected template: %q", project.Template) + } + if !project.NodeCompat { + t.Fatalf("expected node compatibility to be enabled") + } + if project.MainPath() != "src/worker.mjs" { + t.Fatalf("unexpected main path: %q", project.MainPath()) + } + + wranglerCfg, err := store.LoadWrangler(result.ProjectDir, project.WranglerConfig) + if err != nil { + t.Fatalf("load wrangler: %v", err) + } + if wranglerCfg.Main != "src/worker.mjs" { + t.Fatalf("unexpected wrangler main: %q", wranglerCfg.Main) + } + if len(wranglerCfg.CompatibilityFlags) != 1 || wranglerCfg.CompatibilityFlags[0] != "nodejs_compat" { + t.Fatalf("unexpected compatibility flags: %+v", wranglerCfg.CompatibilityFlags) + } + + workerPath := filepath.Join(result.ProjectDir, "src", "worker.mjs") + workerMain, err := filesystem.ReadFile(workerPath) + if err != nil { + t.Fatalf("read js worker: %v", err) + } + if !strings.Contains(string(workerMain), `export default`) { + t.Fatalf("expected JavaScript worker module scaffold, got %s", string(workerMain)) + } +} diff --git a/internal/service/project/templates.go b/internal/service/project/templates.go index dba39c0..f049e6c 100644 --- a/internal/service/project/templates.go +++ b/internal/service/project/templates.go @@ -9,6 +9,14 @@ import ( const defaultGitignore = ".wrangler/\ndist/\nnode_modules/\n" func scaffoldFiles(project config.Project) map[string]string { + if project.EffectiveRuntime() == config.RuntimeJavaScript { + return map[string]string{ + "README.md": readmeTemplate(project), + ".gitignore": defaultGitignore, + "src/worker.mjs": basicJavaScriptWorkerTemplate(), + } + } + return map[string]string{ "README.md": readmeTemplate(project), ".gitignore": defaultGitignore, @@ -68,6 +76,15 @@ func main() { `, message) } +func basicJavaScriptWorkerTemplate() string { + return `export default { + async fetch(request, env, ctx) { + return new Response("hello from flare-edge-cli"); + }, +}; +` +} + func workerShimTemplate(wasmFile string) string { return fmt.Sprintf(`import "./wasm_exec.js"; import wasmModule from "./%s"; diff --git a/internal/service/route/service.go b/internal/service/route/service.go index 656c64c..976e4cd 100644 --- a/internal/service/route/service.go +++ b/internal/service/route/service.go @@ -162,7 +162,7 @@ func contains(value, needle string) bool { } func (s *Service) apply(ctx context.Context, dir, env string) (Result, error) { - if _, err := s.build.Wasm(ctx, buildsvc.WasmOptions{Path: dir}); err != nil { + if _, err := s.build.Build(ctx, buildsvc.WasmOptions{Path: dir}); err != nil { return Result{}, err } raw, err := s.wrangler.Run(ctx, dir, env, "deploy") diff --git a/skills/flare-edge-cli/SKILL.md b/skills/flare-edge-cli/SKILL.md index 0170bcb..a6fad2f 100644 --- a/skills/flare-edge-cli/SKILL.md +++ b/skills/flare-edge-cli/SKILL.md @@ -1,15 +1,16 @@ --- name: flare-edge-cli -description: Use this skill when the task is to scaffold, validate, build, develop, deploy, inspect, operate, or tear down Go-based Cloudflare Workers with flare-edge-cli. Trigger on requests about Cloudflare edge functions, Go/Wasm Workers, Workers AI in Go, or cleanup of ephemeral Cloudflare resources created through flare-edge-cli. +description: Use this skill when the task is to scaffold, validate, build, develop, deploy, inspect, operate, or tear down Cloudflare Workers with flare-edge-cli. Trigger on requests about Cloudflare edge functions, Go/Wasm Workers, native JavaScript Workers, Workers AI in Go, or cleanup of ephemeral Cloudflare resources created through flare-edge-cli. license: MIT metadata: author: Gladium AI - version: 1.0.0 + version: 1.1.0 category: developer-tools tags: - cloudflare - workers - golang + - javascript - wasm - workers-ai - deployment @@ -21,7 +22,7 @@ Use this skill to operate `flare-edge-cli` safely and consistently. ## Use This Skill For -- Creating a new Go-based Cloudflare Worker project +- Creating a new Go-based or native JavaScript Cloudflare Worker project - Validating Go-for-Wasm compatibility before build or deploy - Building and running local dev flows for Workers - Deploying or operating KV, D1, R2, routes, secrets, releases, and logs @@ -41,7 +42,7 @@ Use this skill to operate `flare-edge-cli` safely and consistently. 1. Verify prerequisites. 2. Initialize or inspect the project. -3. Run compatibility and build checks. +3. Run compatibility and build checks when the runtime needs them. 4. Use `dev` for local validation. 5. Use `deploy` for live rollout. 6. Use service-specific commands for KV, D1, R2, secrets, routes, logs, and releases. @@ -50,7 +51,7 @@ Use this skill to operate `flare-edge-cli` safely and consistently. ## Prerequisites - `flare-edge-cli` available on `PATH`, or use the repo-local binary/build path. -- Go installed. +- Go installed for Go/Wasm projects. - Wrangler installed. - Cloudflare auth already configured. @@ -58,7 +59,7 @@ For the latest install flow and release-binary behavior, see [references/install ## Standard Command Path -For a standard Worker: +For a standard Go Worker: ```bash flare-edge-cli doctor --json @@ -69,6 +70,22 @@ flare-edge-cli dev --path ./my-worker --local flare-edge-cli deploy --path ./my-worker --json ``` +For a standard JavaScript Worker: + +```bash +flare-edge-cli doctor --json +flare-edge-cli project init my-js-worker --runtime js +flare-edge-cli build --path ./my-js-worker --json +flare-edge-cli dev --path ./my-js-worker --local +flare-edge-cli deploy --path ./my-js-worker --json +``` + +If the user needs Cloudflare's Node.js compatibility layer, scaffold with: + +```bash +flare-edge-cli project init my-js-worker --runtime js --node-compat +``` + For an AI Worker: ```bash @@ -96,5 +113,6 @@ Use `--keep-bindings` only when the user wants to preserve KV, D1, or R2 resourc - If deployment succeeds but the Worker fails at runtime, use `flare-edge-cli logs tail`. - If a command mutates Cloudflare resources, verify whether the target project path and Worker name are correct before rerunning it. - If a project mixes manual Wrangler edits with CLI-managed config, inspect both `flare-edge.json` and `wrangler.jsonc`. +- `compat check` is only meaningful for Go/Wasm projects; JavaScript Worker projects intentionally skip Go static analysis. For common command sequences and operational guidance, read [references/workflows.md](references/workflows.md). diff --git a/skills/flare-edge-cli/references/workflows.md b/skills/flare-edge-cli/references/workflows.md index 8189510..cdf930c 100644 --- a/skills/flare-edge-cli/references/workflows.md +++ b/skills/flare-edge-cli/references/workflows.md @@ -2,7 +2,7 @@ Use these patterns when operating `flare-edge-cli` projects. -## 1. Bootstrap a Standard Worker +## 1. Bootstrap a Standard Go Worker ```bash flare-edge-cli doctor --json @@ -13,7 +13,23 @@ flare-edge-cli dev --path ./hello-edge --local flare-edge-cli deploy --path ./hello-edge --json ``` -## 2. Bootstrap an AI Worker +## 2. Bootstrap a Standard JavaScript Worker + +```bash +flare-edge-cli doctor --json +flare-edge-cli project init hello-js --runtime js +flare-edge-cli build --path ./hello-js --json +flare-edge-cli dev --path ./hello-js --local +flare-edge-cli deploy --path ./hello-js --json +``` + +To enable Cloudflare's Node.js compatibility layer for the generated Worker: + +```bash +flare-edge-cli project init hello-js --runtime js --node-compat +``` + +## 3. Bootstrap an AI Worker ```bash flare-edge-cli project init hello-ai --template ai-chat @@ -22,7 +38,7 @@ flare-edge-cli dev --path ./hello-ai --local flare-edge-cli deploy --path ./hello-ai --json ``` -## 3. Operate an Existing Project +## 4. Operate an Existing Project ```bash flare-edge-cli project info --path ./hello-edge --json @@ -31,7 +47,15 @@ flare-edge-cli build --path ./hello-edge --json flare-edge-cli logs tail --path ./hello-edge ``` -## 4. Provision Data Resources +For a JavaScript project, skip `compat check`: + +```bash +flare-edge-cli project info --path ./hello-js --json +flare-edge-cli build --path ./hello-js --json +flare-edge-cli logs tail --path ./hello-js +``` + +## 5. Provision Data Resources ```bash flare-edge-cli kv namespace create CACHE --path ./hello-edge --json @@ -39,7 +63,7 @@ flare-edge-cli d1 create DB --path ./hello-edge --json flare-edge-cli r2 bucket create FILES --path ./hello-edge --json ``` -## 5. Safe Cleanup +## 6. Safe Cleanup For temporary resources created during tests or agent workflows: