@@ -44,8 +44,8 @@ func (p *ContributorPlugin) Commands() []cli.Command {
4444 "new" ,
4545 "Scaffold a new dashboard contributor" ,
4646 p .newContributor ,
47- cli .WithFlag (cli .NewStringFlag ("framework" , "f" , "UI framework (astro, nextjs)" , "" )),
48- cli .WithFlag (cli .NewStringFlag ("mode" , "m" , "Build mode (static, ssr)" , "static" )),
47+ cli .WithFlag (cli .NewStringFlag ("framework" , "f" , "UI framework (templ, astro, nextjs)" , "" )),
48+ cli .WithFlag (cli .NewStringFlag ("mode" , "m" , "Build mode (static, ssr, local )" , "static" )),
4949 cli .WithFlag (cli .NewBoolFlag ("in-extension" , "e" , "Scaffold inside an existing extension directory" , false )),
5050 ))
5151
@@ -105,22 +105,30 @@ func (p *ContributorPlugin) newContributor(ctx cli.CommandContext) error {
105105 framework := ctx .String ("framework" )
106106 if framework == "" {
107107 var err error
108- framework , err = ctx .Select ("UI framework:" , []string {"astro" , "nextjs" })
108+ framework , err = ctx .Select ("UI framework:" , []string {"templ (recommended)" , " astro" , "nextjs" })
109109 if err != nil {
110110 return err
111111 }
112+ // Normalize the selection (strip suffix)
113+ if framework == "templ (recommended)" {
114+ framework = "templ"
115+ }
112116 }
113117 if ! contribConfig .ValidFrameworkTypes [framework ] {
114- return fmt .Errorf ("unsupported framework: %s (use astro, nextjs, or custom)" , framework )
118+ return fmt .Errorf ("unsupported framework: %s (use templ, astro, nextjs, or custom)" , framework )
115119 }
116120
117121 // Get build mode
118122 mode := ctx .String ("mode" )
119123 if mode == "" {
120- mode = "static"
124+ if framework == "templ" {
125+ mode = "local"
126+ } else {
127+ mode = "static"
128+ }
121129 }
122130 if ! contribConfig .ValidBuildModes [mode ] {
123- return fmt .Errorf ("unsupported build mode: %s (use static or ssr )" , mode )
131+ return fmt .Errorf ("unsupported build mode: %s (use static, ssr, or local )" , mode )
124132 }
125133
126134 // Determine target directory
@@ -143,10 +151,17 @@ func (p *ContributorPlugin) newContributor(ctx cli.CommandContext) error {
143151 spinner := ctx .Spinner (fmt .Sprintf ("Scaffolding %s contributor with %s (%s)..." , name , framework , mode ))
144152
145153 // Create directory structure
146- uiDir := filepath .Join (targetDir , "ui" )
147- if err := os .MkdirAll (uiDir , 0755 ); err != nil {
148- spinner .Stop (cli .Red ("✗ Failed" ))
149- return err
154+ if framework != "templ" {
155+ uiDir := filepath .Join (targetDir , "ui" )
156+ if err := os .MkdirAll (uiDir , 0755 ); err != nil {
157+ spinner .Stop (cli .Red ("✗ Failed" ))
158+ return err
159+ }
160+ } else {
161+ if err := os .MkdirAll (targetDir , 0755 ); err != nil {
162+ spinner .Stop (cli .Red ("✗ Failed" ))
163+ return err
164+ }
150165 }
151166
152167 // Prepare template data
@@ -179,7 +194,13 @@ func (p *ContributorPlugin) newContributor(ctx cli.CommandContext) error {
179194 }
180195
181196 // Framework-specific scaffolding
197+ uiDir := filepath .Join (targetDir , "ui" )
182198 switch framework {
199+ case "templ" :
200+ if err := p .scaffoldTempl (targetDir , data ); err != nil {
201+ spinner .Stop (cli .Red ("✗ Failed" ))
202+ return err
203+ }
183204 case "astro" :
184205 if err := p .scaffoldAstro (uiDir , data ); err != nil {
185206 spinner .Stop (cli .Red ("✗ Failed" ))
@@ -196,10 +217,42 @@ func (p *ContributorPlugin) newContributor(ctx cli.CommandContext) error {
196217
197218 ctx .Println ("" )
198219 ctx .Success ("Next steps:" )
199- ctx .Println (fmt .Sprintf (" 1. cd %s/ui && npm install" , relPath (p .config .RootDir , targetDir )))
200- ctx .Println (" 2. Edit UI pages in ui/src/pages/ (Astro) or ui/app/ (Next.js)" )
201- ctx .Println (fmt .Sprintf (" 3. forge contributor build %s" , name ))
202- ctx .Println (fmt .Sprintf (" 4. forge contributor dev %s # for live development" , name ))
220+ if framework == "templ" {
221+ ctx .Println (fmt .Sprintf (" 1. cd %s" , relPath (p .config .RootDir , targetDir )))
222+ ctx .Println (" 2. Edit .templ files (pages.templ, widgets.templ, settings.templ)" )
223+ ctx .Println (" 3. templ generate # generate Go code from .templ files" )
224+ ctx .Println (" 4. Register the contributor in your extension's init()" )
225+ } else {
226+ ctx .Println (fmt .Sprintf (" 1. cd %s/ui && npm install" , relPath (p .config .RootDir , targetDir )))
227+ ctx .Println (" 2. Edit UI pages in ui/src/pages/ (Astro) or ui/app/ (Next.js)" )
228+ ctx .Println (fmt .Sprintf (" 3. forge contributor build %s" , name ))
229+ ctx .Println (fmt .Sprintf (" 4. forge contributor dev %s # for live development" , name ))
230+ }
231+
232+ return nil
233+ }
234+
235+ // scaffoldTempl creates templ-specific project files directly in the contributor directory.
236+ func (p * ContributorPlugin ) scaffoldTempl (targetDir string , data scaffoldTemplateData ) error {
237+ // Write contributor.go (main Go file with RenderPage/RenderWidget/RenderSettings)
238+ if err := writeTemplate (filepath .Join (targetDir , "contributor.go" ), templContributorGoTemplate , data ); err != nil {
239+ return err
240+ }
241+
242+ // Write pages.templ (sample overview page)
243+ if err := writeTemplate (filepath .Join (targetDir , "pages.templ" ), templPageTemplate , data ); err != nil {
244+ return err
245+ }
246+
247+ // Write widgets.templ (sample status widget)
248+ if err := writeTemplate (filepath .Join (targetDir , "widgets.templ" ), templWidgetTemplate , data ); err != nil {
249+ return err
250+ }
251+
252+ // Write settings.templ (sample settings panel)
253+ if err := writeTemplate (filepath .Join (targetDir , "settings.templ" ), templSettingsTemplate , data ); err != nil {
254+ return err
255+ }
203256
204257 return nil
205258}
@@ -331,8 +384,8 @@ func (p *ContributorPlugin) buildSingleContributor(ctx cli.CommandContext, dir s
331384
332385 ctx .Info (fmt .Sprintf ("Building contributor: %s (%s, %s mode)" , cfg .DisplayName , cfg .Type , cfg .Build .Mode ))
333386
334- // Step 1: npm install (if requested and node_modules doesn't exist or flag set )
335- if install {
387+ // Step 1: npm install (if requested, not templ, and node_modules doesn't exist)
388+ if install && cfg . Type != "templ" {
336389 if _ , err := os .Stat (filepath .Join (uiDir , "node_modules" )); os .IsNotExist (err ) {
337390 spinner := ctx .Spinner ("Installing dependencies..." )
338391 pm := detectPackageManager (uiDir )
@@ -359,9 +412,15 @@ func (p *ContributorPlugin) buildSingleContributor(ctx cli.CommandContext, dir s
359412 buildCmd = adapter .DefaultBuildCmd (cfg .Build .Mode )
360413 }
361414
415+ // For templ, run the build command in the contributor dir; for others, in the ui dir
416+ buildDir := uiDir
417+ if cfg .Type == "templ" {
418+ buildDir = configDir
419+ }
420+
362421 spinner := ctx .Spinner (fmt .Sprintf ("Building %s UI..." , cfg .Type ))
363422 cmd := exec .Command ("sh" , "-c" , buildCmd )
364- cmd .Dir = uiDir
423+ cmd .Dir = buildDir
365424 cmd .Stdout = os .Stdout
366425 cmd .Stderr = os .Stderr
367426 if err := cmd .Run (); err != nil {
@@ -370,10 +429,12 @@ func (p *ContributorPlugin) buildSingleContributor(ctx cli.CommandContext, dir s
370429 }
371430 spinner .Stop (cli .Green ("✓ UI built" ))
372431
373- // Step 3: Validate build output
374- distPath := cfg .DistPath (configDir )
375- if err := adapter .ValidateBuild (distPath ); err != nil {
376- return fmt .Errorf ("build validation failed: %w" , err )
432+ // Step 3: Validate build output (skip for templ — compiled into Go binary)
433+ if cfg .Type != "templ" {
434+ distPath := cfg .DistPath (configDir )
435+ if err := adapter .ValidateBuild (distPath ); err != nil {
436+ return fmt .Errorf ("build validation failed: %w" , err )
437+ }
377438 }
378439
379440 // Step 4: Run codegen
@@ -463,19 +524,21 @@ func (p *ContributorPlugin) devContributor(ctx cli.CommandContext) error {
463524 cfgDir := filepath .Dir (yamlPath )
464525 uiDir := cfg .UIPath (cfgDir )
465526
466- // Ensure dependencies are installed
467- if _ , err := os .Stat (filepath .Join (uiDir , "node_modules" )); os .IsNotExist (err ) {
468- spinner := ctx .Spinner ("Installing dependencies..." )
469- pm := detectPackageManager (uiDir )
470- cmd := exec .Command ("sh" , "-c" , installCmd (pm ))
471- cmd .Dir = uiDir
472- cmd .Stdout = os .Stdout
473- cmd .Stderr = os .Stderr
474- if err := cmd .Run (); err != nil {
475- spinner .Stop (cli .Red ("✗ Install failed" ))
476- return fmt .Errorf ("npm install failed: %w" , err )
527+ // Ensure dependencies are installed (skip for templ)
528+ if cfg .Type != "templ" {
529+ if _ , err := os .Stat (filepath .Join (uiDir , "node_modules" )); os .IsNotExist (err ) {
530+ spinner := ctx .Spinner ("Installing dependencies..." )
531+ pm := detectPackageManager (uiDir )
532+ cmd := exec .Command ("sh" , "-c" , installCmd (pm ))
533+ cmd .Dir = uiDir
534+ cmd .Stdout = os .Stdout
535+ cmd .Stderr = os .Stderr
536+ if err := cmd .Run (); err != nil {
537+ spinner .Stop (cli .Red ("✗ Install failed" ))
538+ return fmt .Errorf ("npm install failed: %w" , err )
539+ }
540+ spinner .Stop (cli .Green ("✓ Dependencies installed" ))
477541 }
478- spinner .Stop (cli .Green ("✓ Dependencies installed" ))
479542 }
480543
481544 // Determine dev command
@@ -489,19 +552,25 @@ func (p *ContributorPlugin) devContributor(ctx cli.CommandContext) error {
489552 devCmd = adapter .DefaultDevCmd ()
490553 }
491554
492- // Add port if specified
555+ // Add port if specified (not applicable for templ)
493556 port := ctx .Int ("port" )
494- if port > 0 {
557+ if port > 0 && cfg . Type != "templ" {
495558 devCmd = fmt .Sprintf ("%s --port %d" , devCmd , port )
496559 }
497560
561+ // For templ, run in contributor dir; for others, in ui dir
562+ runDir := uiDir
563+ if cfg .Type == "templ" {
564+ runDir = cfgDir
565+ }
566+
498567 ctx .Info (fmt .Sprintf ("Starting %s dev server for %s..." , cfg .Type , cfg .DisplayName ))
499- ctx .Info (fmt .Sprintf (" UI dir : %s" , relPath (p .config .RootDir , uiDir )))
568+ ctx .Info (fmt .Sprintf (" Dir : %s" , relPath (p .config .RootDir , runDir )))
500569 ctx .Println ("" )
501570
502571 // Run dev server (blocking — user Ctrl+C to stop)
503572 cmd := exec .Command ("sh" , "-c" , devCmd )
504- cmd .Dir = uiDir
573+ cmd .Dir = runDir
505574 cmd .Stdout = os .Stdout
506575 cmd .Stderr = os .Stderr
507576 cmd .Stdin = os .Stdin
0 commit comments