@@ -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
+ }
64
85
65
- frontMatter , err := pageparser .ParseFrontMatterAndContent (bytes .NewReader (readme ))
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
+ }
94
+
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
+ //nolint:nilnil // This is a script, not a library.
125
+ return nil , nil
126
+ }
79
127
80
- descriptionRaw , exists := frontMatter .FrontMatter ["description" ]
81
- if ! exists {
82
- return xerrors .Errorf ("example %q front matter does not contain name" , exampleID )
83
- }
128
+ exampleID := dir .Name ()
129
+ // Each one of these is a example!
130
+ readme , err := fs .ReadFile (examplesFS , path .Join (name , "README.md" ))
131
+ if err != nil {
132
+ return nil , xerrors .New ("missing README.md" )
133
+ }
84
134
85
- description , valid := descriptionRaw .( string )
86
- if ! valid {
87
- return xerrors .Errorf ("example %q description isn't a string " , exampleID )
88
- }
135
+ frontMatter , err := pageparser . ParseFrontMatterAndContent ( bytes . NewReader ( readme ) )
136
+ if err != nil {
137
+ return nil , xerrors .Errorf ("parse front matter: %w " , err )
138
+ }
89
139
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
- }
140
+ // Make sure validation here is in sync with requirements for
141
+ // coder/registry.
142
+ displayName , err := getString (frontMatter .FrontMatter , "display_name" )
143
+ if err != nil {
144
+ errs = append (errs , err )
145
+ }
146
+
147
+ description , err := getString (frontMatter .FrontMatter , "description" )
148
+ if err != nil {
149
+ errs = append (errs , err )
150
+ }
151
+
152
+ _ , err = getString (frontMatter .FrontMatter , "maintainer_github" )
153
+ if err != nil {
154
+ errs = append (errs , err )
155
+ }
156
+
157
+ tags := []string {}
158
+ tagsRaw , exists := frontMatter .FrontMatter ["tags" ]
159
+ if exists {
160
+ tagsI , valid := tagsRaw .([]interface {})
161
+ if ! valid {
162
+ errs = append (errs , xerrors .Errorf ("tags isn't a slice: type %T" , tagsRaw ))
163
+ } else {
97
164
for _ , tagI := range tagsI {
98
165
tag , valid := tagI .(string )
99
166
if ! valid {
100
- return xerrors .Errorf ("example %q tag isn't a string: type %T" , exampleID , tagI )
167
+ errs = append (errs , xerrors .Errorf ("tag isn't a string: type %T" , tagI ))
168
+ continue
101
169
}
102
170
tags = append (tags , tag )
103
171
}
104
172
}
173
+ }
105
174
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 )
112
- }
113
- icon , err = filepath .Rel ("../site/static/" , filepath .Join (examplesDir , name , icon ))
114
- if err != nil {
115
- return xerrors .Errorf ("example %q icon is not in site/static: %w" , exampleID , err )
116
- }
117
- // The FE needs a static path!
118
- icon = "/" + icon
175
+ var icon string
176
+ icon , err = getString (frontMatter .FrontMatter , "icon" )
177
+ if err != nil {
178
+ errs = append (errs , err )
179
+ } else {
180
+ cleanPath := filepath .Clean (filepath .Join (examplesDir , name , icon ))
181
+ _ , err := fs .Stat (projectFS , cleanPath )
182
+ if err != nil {
183
+ errs = append (errs , xerrors .Errorf ("icon does not exist: %w" , err ))
119
184
}
185
+ if ! strings .HasPrefix (cleanPath , filepath .Join ("site" , "static" )) {
186
+ errs = append (errs , xerrors .Errorf ("icon is not in site/static/: %q" , icon ))
187
+ }
188
+ icon , err = filepath .Rel (filepath .Join ("site" , "static" ), cleanPath )
189
+ if err != nil {
190
+ errs = append (errs , xerrors .Errorf ("cannot make icon relative to site/static: %w" , err ))
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 ) {
0 commit comments