@@ -3,9 +3,12 @@ package main
3
3
import (
4
4
"bytes"
5
5
"encoding/json"
6
+ "errors"
7
+ "flag"
6
8
"fmt"
7
9
"go/parser"
8
10
"go/token"
11
+ "io"
9
12
"io/fs"
10
13
"os"
11
14
"path"
@@ -24,122 +27,196 @@ const (
24
27
)
25
28
26
29
func main () {
27
- if err := run ();err != nil {
28
- panic (err )
30
+ lint := flag .Bool ("lint" ,false ,"Lint **all** the examples instead of generating the examples.gen.json file" )
31
+ flag .Parse ()
32
+
33
+ if err := run (* lint );err != nil {
34
+ _ ,_ = fmt .Fprintf (os .Stderr ,"error: %+v\n " ,err )
35
+ os .Exit (1 )
29
36
}
30
37
}
31
38
32
- func run ()error {
39
+ //nolint:revive // This is a script, not a library.
40
+ func run (lint bool )error {
33
41
fset := token .NewFileSet ()
34
42
src ,err := parser .ParseFile (fset ,filepath .Join (examplesDir ,examplesSrc ),nil ,parser .ParseComments )
35
43
if err != nil {
36
44
return err
37
45
}
38
46
47
+ projectFS := os .DirFS ("." )
48
+ examplesFS := os .DirFS (examplesDir )
49
+
39
50
var paths []string
40
- for _ ,comment := range src .Comments {
41
- for _ ,line := range comment .List {
42
- if s ,ok := parseEmbedTag (line .Text );ok && ! strings .HasSuffix (s ,".json" ) {
43
- paths = append (paths ,s )
51
+ if lint {
52
+ files ,err := fs .ReadDir (examplesFS ,"templates" )
53
+ if err != nil {
54
+ return err
55
+ }
56
+
57
+ for _ ,f := range files {
58
+ if ! f .IsDir () {
59
+ continue
60
+ }
61
+ paths = append (paths ,filepath .Join ("templates" ,f .Name ()))
62
+ }
63
+ }else {
64
+ for _ ,comment := range src .Comments {
65
+ for _ ,line := range comment .List {
66
+ if s ,ok := parseEmbedTag (line .Text );ok && ! strings .HasSuffix (s ,".json" ) {
67
+ paths = append (paths ,s )
68
+ }
44
69
}
45
70
}
46
71
}
47
72
48
73
var examples []codersdk.TemplateExample
49
- files := os . DirFS ( examplesDir )
74
+ var errs [] error
50
75
for _ ,name := range paths {
51
- dir ,err := fs . Stat ( files ,name )
76
+ te ,err := parseTemplateExample ( projectFS , examplesFS ,name )
52
77
if err != nil {
53
- return err
54
- }
55
- if ! dir .IsDir () {
78
+ errs = append (errs ,err )
56
79
continue
57
80
}
58
- exampleID := dir .Name ()
59
- // Each one of these is a example!
60
- readme ,err := fs .ReadFile (files ,path .Join (name ,"README.md" ))
61
- if err != nil {
62
- return xerrors .Errorf ("example %q does not contain README.md" ,exampleID )
81
+ if te != nil {
82
+ examples = append (examples ,* te )
63
83
}
84
+ }
85
+
86
+ if len (errs )> 0 {
87
+ return xerrors .Errorf ("parse failed: %w" ,errors .Join (errs ... ))
88
+ }
89
+
90
+ var w io.Writer = os .Stdout
91
+ if lint {
92
+ w = io .Discard
93
+ }
64
94
65
- frontMatter ,err := pageparser .ParseFrontMatterAndContent (bytes .NewReader (readme ))
95
+ _ ,err = fmt .Fprint (w ,"// Code generated by examplegen. DO NOT EDIT.\n " )
96
+ if err != nil {
97
+ return err
98
+ }
99
+
100
+ enc := json .NewEncoder (w )
101
+ enc .SetIndent ("" ," " )
102
+ return enc .Encode (examples )
103
+ }
104
+
105
+ func parseTemplateExample (projectFS ,examplesFS fs.FS ,name string ) (te * codersdk.TemplateExample ,err error ) {
106
+ var errs []error
107
+ defer func () {
66
108
if err != nil {
67
- return xerrors . Errorf ( "parse example %q front matter: %w" , exampleID , err )
109
+ errs = append ([] error { err }, errs ... )
68
110
}
69
-
70
- nameRaw ,exists := frontMatter .FrontMatter ["display_name" ]
71
- if ! exists {
72
- return xerrors .Errorf ("example %q front matter does not contain name" ,exampleID )
111
+ if len (errs )> 0 {
112
+ err = xerrors .Errorf ("example %q has errors" ,name )
113
+ for _ ,e := range errs {
114
+ err = errors .Join (err ,e )
115
+ }
73
116
}
117
+ }()
74
118
75
- name ,valid := nameRaw .(string )
76
- if ! valid {
77
- return xerrors .Errorf ("example %q name isn't a string" ,exampleID )
78
- }
119
+ dir ,err := fs .Stat (examplesFS ,name )
120
+ if err != nil {
121
+ return nil ,err
122
+ }
123
+ if ! dir .IsDir () {
124
+ return nil ,nil
125
+ }
79
126
80
- descriptionRaw ,exists := frontMatter .FrontMatter ["description" ]
81
- if ! exists {
82
- return xerrors .Errorf ("example %q front matter does not contain name" ,exampleID )
83
- }
127
+ exampleID := dir .Name ()
128
+ // Each one of these is a example!
129
+ readme ,err := fs .ReadFile (examplesFS ,path .Join (name ,"README.md" ))
130
+ if err != nil {
131
+ return nil ,xerrors .New ("missing README.md" )
132
+ }
84
133
85
- description , valid := descriptionRaw .( string )
86
- if ! valid {
87
- return xerrors .Errorf ("example %q description isn't a string " ,exampleID )
88
- }
134
+ frontMatter , err := pageparser . ParseFrontMatterAndContent ( bytes . NewReader ( readme ) )
135
+ if err != nil {
136
+ return nil , xerrors .Errorf ("parse front matter: %w " ,err )
137
+ }
89
138
90
- tags := []string {}
91
- tagsRaw ,exists := frontMatter .FrontMatter ["tags" ]
92
- if exists {
93
- tagsI ,valid := tagsRaw .([]interface {})
94
- if ! valid {
95
- return xerrors .Errorf ("example %q tags isn't a slice: type %T" ,exampleID ,tagsRaw )
96
- }
139
+ // Make sure validation here is in sync with requirements for
140
+ // coder/registry.
141
+ displayName ,err := getString (frontMatter .FrontMatter ,"display_name" )
142
+ if err != nil {
143
+ errs = append (errs ,err )
144
+ }
145
+
146
+ description ,err := getString (frontMatter .FrontMatter ,"description" )
147
+ if err != nil {
148
+ errs = append (errs ,err )
149
+ }
150
+
151
+ _ ,err = getString (frontMatter .FrontMatter ,"maintainer_github" )
152
+ if err != nil {
153
+ errs = append (errs ,err )
154
+ }
155
+
156
+ tags := []string {}
157
+ tagsRaw ,exists := frontMatter .FrontMatter ["tags" ]
158
+ if exists {
159
+ tagsI ,valid := tagsRaw .([]interface {})
160
+ if ! valid {
161
+ errs = append (errs ,xerrors .Errorf ("tags isn't a slice: type %T" ,tagsRaw ))
162
+ }else {
97
163
for _ ,tagI := range tagsI {
98
164
tag ,valid := tagI .(string )
99
165
if ! valid {
100
- return xerrors .Errorf ("example %q tag isn't a string: type %T" ,exampleID ,tagI )
166
+ errs = append (errs ,xerrors .Errorf ("tag isn't a string: type %T" ,tagI ))
167
+ continue
101
168
}
102
169
tags = append (tags ,tag )
103
170
}
104
171
}
172
+ }
105
173
106
- var icon string
107
- iconRaw ,exists := frontMatter .FrontMatter ["icon" ]
108
- if exists {
109
- icon ,valid = iconRaw .(string )
110
- if ! valid {
111
- return xerrors .Errorf ("example %q icon isn't a string" ,exampleID )
174
+ var icon string
175
+ iconRaw ,exists := frontMatter .FrontMatter ["icon" ]
176
+ if exists {
177
+ var valid bool
178
+ icon ,valid = iconRaw .(string )
179
+ if ! valid {
180
+ errs = append (errs ,xerrors .Errorf ("icon isn't a string: type %T" ,iconRaw ))
181
+ }else {
182
+ cleanPath := filepath .Clean (filepath .Join (examplesDir ,name ,icon ))
183
+ _ ,err := fs .Stat (projectFS ,cleanPath )
184
+ if err != nil {
185
+ errs = append (errs ,xerrors .Errorf ("icon does not exist: %w" ,err ))
112
186
}
113
- icon ,err = filepath .Rel ("../ site/static/" ,filepath . Join ( examplesDir , name , icon ) )
187
+ icon ,err = filepath .Rel ("site/static/" ,cleanPath )
114
188
if err != nil {
115
- return xerrors .Errorf ("example %q icon is not in site/static: %w" ,exampleID , err )
189
+ errs = append ( errs , xerrors .Errorf ("icon is not in site/static: %w" ,err ) )
116
190
}
117
- // The FE needs a static path!
118
- icon = "/" + icon
119
191
}
192
+ }
120
193
121
- examples = append (examples , codersdk.TemplateExample {
122
- ID :exampleID ,
123
- Name :name ,
124
- Description :description ,
125
- Icon :icon ,
126
- Tags :tags ,
127
- Markdown :string (frontMatter .Content ),
128
-
129
- // URL is set by examples/examples.go.
130
- })
194
+ if len (errs )> 0 {
195
+ return nil ,xerrors .New ("front matter validation failed" )
131
196
}
132
197
133
- w := os .Stdout
198
+ return & codersdk.TemplateExample {
199
+ ID :exampleID ,
200
+ Name :displayName ,
201
+ Description :description ,
202
+ Icon :"/" + icon ,// The FE needs a static path!
203
+ Tags :tags ,
204
+ Markdown :string (frontMatter .Content ),
134
205
135
- _ ,err = fmt .Fprint (w ,"// Code generated by examplegen. DO NOT EDIT.\n " )
136
- if err != nil {
137
- return err
138
- }
206
+ // URL is set by examples/examples.go.
207
+ },nil
208
+ }
139
209
140
- enc := json .NewEncoder (os .Stdout )
141
- enc .SetIndent ("" ," " )
142
- return enc .Encode (examples )
210
+ func getString (m map [string ]any ,key string ) (string ,error ) {
211
+ v ,ok := m [key ]
212
+ if ! ok {
213
+ return "" ,xerrors .Errorf ("front matter does not contain %q" ,key )
214
+ }
215
+ vv ,ok := v .(string )
216
+ if ! ok {
217
+ return "" ,xerrors .Errorf ("%q isn't a string" ,key )
218
+ }
219
+ return vv ,nil
143
220
}
144
221
145
222
func parseEmbedTag (s string ) (string ,bool ) {