|
1 | | -(ns eden.report |
2 | | - (:require [replicant.string :as rs] |
3 | | - [clojure.string :as str] |
4 | | - [eden.loader :as loader])) |
| 1 | +(ns eden.report) |
5 | 2 |
|
6 | | -(defn- format-file-size |
7 | | - "Format bytes into human-readable size" |
8 | | - [bytes] |
9 | | - (cond |
10 | | - (< bytes 1024) (format "%db" bytes) |
11 | | - (< bytes (* 1024 1024)) (format "%.1fkb" (/ bytes 1024.0)) |
12 | | - :else (format "%.1fmb" (/ bytes (* 1024.0 1024.0))))) |
| 3 | +(defmulti print-warning :type) |
13 | 4 |
|
14 | | -(defn generate-html-report |
15 | | - "Generate an HTML build report for dev mode" |
16 | | - [{:keys [timings warnings results error site-edn mode]}] |
17 | | - (let [total-time (reduce + 0 (vals timings)) |
18 | | - html-count (get-in results [:write-output :html-count] 0) |
19 | | - timestamp (java.time.LocalDateTime/now) |
20 | | - status (if error "failed" "success") |
21 | | - max-time (apply max 1 (vals timings))] |
22 | | - (str "<!DOCTYPE html>" |
23 | | - (rs/render |
24 | | - [:html |
25 | | - [:head |
26 | | - [:title "Build Report"] |
27 | | - [:meta {:charset "UTF-8"}] |
28 | | - [:style "body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
29 | | - max-width: 1200px; margin: 0 auto; padding: 20px; background: #f5f5f5; } |
30 | | - .header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; |
31 | | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); } |
32 | | - .status { display: inline-block; padding: 4px 12px; border-radius: 4px; |
33 | | - font-weight: 600; font-size: 14px; } |
34 | | - .status.success { background: #d4edda; color: #155724; } |
35 | | - .status.failed { background: #f8d7da; color: #721c24; } |
36 | | - .info { color: #666; margin: 5px 0; font-size: 14px; } |
37 | | - .section { background: white; padding: 20px; border-radius: 8px; |
38 | | - margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } |
39 | | - h2 { margin-top: 0; color: #333; border-bottom: 2px solid #e9ecef; padding-bottom: 10px; } |
40 | | - .timing-table { width: 100%; border-collapse: collapse; } |
41 | | - .timing-table th { text-align: left; padding: 8px; background: #f8f9fa; |
42 | | - border-bottom: 2px solid #dee2e6; } |
43 | | - .timing-table td { padding: 8px; border-bottom: 1px solid #dee2e6; } |
44 | | - .timing-bar { background: #007bff; height: 20px; border-radius: 3px; |
45 | | - min-width: 2px; display: inline-block; } |
46 | | - .warning { background: #fff3cd; border-left: 4px solid #ffc107; |
47 | | - padding: 12px; margin: 10px 0; border-radius: 4px; } |
48 | | - .warning-title { font-weight: 600; color: #856404; margin-bottom: 8px; } |
49 | | - .warning-list { margin: 5px 0 5px 20px; color: #856404; } |
50 | | - .error { background: #f8d7da; border-left: 4px solid #dc3545; |
51 | | - padding: 12px; margin: 10px 0; border-radius: 4px; color: #721c24; } |
52 | | - .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
53 | | - gap: 15px; margin-top: 20px; } |
54 | | - .stat-card { background: #f8f9fa; padding: 15px; border-radius: 6px; } |
55 | | - .stat-value { font-size: 24px; font-weight: 600; color: #333; } |
56 | | - .stat-label { color: #666; font-size: 14px; margin-top: 5px; }"]] |
57 | | - [:body |
58 | | - [:div.header |
59 | | - [:h1 "Build Report"] |
60 | | - [:span.status {:class status} (str/upper-case status)] |
61 | | - [:div.info "Generated: " (str timestamp)] |
62 | | - [:div.info "Site: " site-edn] |
63 | | - [:div.info "Mode: " (name mode)]] |
| 5 | +(defmethod print-warning :missing-key [{:keys [directive key template]}] |
| 6 | + (println (format " Missing key: %s for directive %s in template %s" key directive template))) |
64 | 7 |
|
65 | | - (when error |
66 | | - [:div.section |
67 | | - [:h2 "❌ Build Error"] |
68 | | - [:div.error error]]) |
| 8 | +(defmethod print-warning :default [{:keys [type] :as warning}] |
| 9 | + (println "Unknown warning:" type) |
| 10 | + (prn warning)) |
69 | 11 |
|
70 | | - [:div.section |
71 | | - [:h2 "⏱️ Build Performance"] |
72 | | - [:table.timing-table |
73 | | - [:thead |
74 | | - [:tr |
75 | | - [:th "Step"] |
76 | | - [:th "Time (ms)"] |
77 | | - [:th {:style "width: 50%"} "Timeline"]]] |
78 | | - [:tbody |
79 | | - (for [[step elapsed] (sort-by val > timings)] |
80 | | - [:tr |
81 | | - [:td (name step)] |
82 | | - [:td elapsed] |
83 | | - [:td |
84 | | - [:div.timing-bar |
85 | | - {:style (str "width: " (* 100 (/ elapsed max-time)) "%")}]]]) |
86 | | - [:tr {:style "font-weight: 600; background: #f8f9fa;"} |
87 | | - [:td "Total"] |
88 | | - [:td total-time] |
89 | | - [:td]]]]] |
| 12 | +(defn print-build-report [ctx] |
| 13 | + (when-let [warnings (seq (into #{} (:warnings ctx)))] |
| 14 | + (println "\n⚠️ Warnings") |
| 15 | + (doseq [warning warnings] |
| 16 | + (print-warning warning))) |
90 | 17 |
|
91 | | - [:div.section |
92 | | - [:h2 "📊 Build Statistics"] |
93 | | - [:div.stats |
94 | | - [:div.stat-card |
95 | | - [:div.stat-value html-count] |
96 | | - [:div.stat-label "HTML Files Generated"]] |
97 | | - [:div.stat-card |
98 | | - [:div.stat-value (count timings)] |
99 | | - [:div.stat-label "Build Steps"]] |
100 | | - [:div.stat-card |
101 | | - [:div.stat-value (+ (count (:missing-keys warnings [])) |
102 | | - (count (:missing-pages warnings [])))] |
103 | | - [:div.stat-label "Total Warnings"]] |
104 | | - (when-let [copied (get-in results [:copy-static :copied])] |
105 | | - [:div.stat-card |
106 | | - [:div.stat-value copied] |
107 | | - [:div.stat-label "Static Files Copied"]])]] |
108 | | - |
109 | | - (when (or (seq (:missing-keys warnings)) |
110 | | - (seq (:missing-pages warnings))) |
111 | | - [:div.section |
112 | | - [:h2 "⚠️ Warnings"] |
113 | | - |
114 | | - (when (seq (:missing-keys warnings)) |
115 | | - [:div.warning |
116 | | - [:div.warning-title "Missing Template Keys"] |
117 | | - (let [grouped (group-by :template (:missing-keys warnings))] |
118 | | - (for [[template keys] grouped] |
119 | | - [:div |
120 | | - [:strong "In " template ":"] |
121 | | - [:ul.warning-list |
122 | | - (for [{:keys [key page-id]} keys] |
123 | | - [:li key " (rendering " page-id ")"])]]))]) |
124 | | - |
125 | | - (when (seq (:missing-pages warnings)) |
126 | | - [:div.warning |
127 | | - [:div.warning-title "Missing Page References"] |
128 | | - [:ul.warning-list |
129 | | - (for [{:keys [page-id template]} (:missing-pages warnings)] |
130 | | - [:li "Page ID: " [:code page-id] |
131 | | - (when template (str " (referenced in " template ")"))])]])]) |
132 | | - |
133 | | - [:div {:style "text-align: center; color: #999; padding: 20px; font-size: 12px;"} |
134 | | - "Generated by Anteo Website Builder"]]])))) |
135 | | - |
136 | | -(defn print-build-report |
137 | | - "Print a formatted build report with timings and warnings" |
138 | | - [{:keys [timings warnings results error] :as ctx}] |
139 | | - (println " Build steps:") |
140 | | - |
141 | | - ;; Print each step timing |
142 | | - (doseq [[step elapsed] timings] |
143 | | - (println (format " %-20s %dms" (name step) elapsed))) |
144 | | - |
145 | | - ;; Print totals |
146 | | - (println (format " Total time: %dms" (reduce + (vals timings)))) |
147 | | - (when-let [html-count (get-in results [:write-output :html-count])] |
148 | | - (println (format " Generated %d HTML files" html-count))) |
149 | | - |
150 | | - ;; Print bundle info if available |
151 | | - (when-let [bundle-info (get-in results [:bundle-assets])] |
152 | | - (let [css-files (get-in bundle-info [:css :files]) |
153 | | - js-files (get-in bundle-info [:js :files]) |
154 | | - all-files (concat css-files js-files)] |
155 | | - (when (seq all-files) |
156 | | - (println "\n Bundled assets:") |
157 | | - (doseq [{:keys [file size type]} all-files] |
158 | | - (println (format " %s/%s %s" |
159 | | - (name type) |
160 | | - file |
161 | | - (format-file-size size))))))) |
162 | | - |
163 | | - ;; Group warnings by type |
164 | | - (let [page-warnings (:page-warnings warnings) |
165 | | - warnings-by-type (group-by :type page-warnings) |
166 | | - unconfigured-langs (:unconfigured-language warnings-by-type) |
167 | | - ;; Get all warnings that are NOT unconfigured-language |
168 | | - other-warnings (mapcat val (dissoc warnings-by-type :unconfigured-language)) |
169 | | - ;; Group unconfigured language warnings by language |
170 | | - langs-by-lang (group-by :lang unconfigured-langs)] |
171 | | - |
172 | | - ;; Print unconfigured language warnings (grouped) |
173 | | - (when (seq langs-by-lang) |
174 | | - (println "\n⚠️ Language configuration issues:") |
175 | | - (doseq [[lang lang-warnings] langs-by-lang] |
176 | | - (let [first-warning (first lang-warnings) |
177 | | - page-count (count lang-warnings)] |
178 | | - ;; Use the helpful message from the first warning |
179 | | - (if-let [message (:message first-warning)] |
180 | | - (println (format " - %s" message)) |
181 | | - (println (format " - Language '%s' not configured (%d pages affected)" |
182 | | - (name lang) page-count))) |
183 | | - ;; Don't list individual pages when there are many |
184 | | - (when (<= page-count 5) |
185 | | - (doseq [w lang-warnings] |
186 | | - (println (format " Page: %s" (:content-key w)))))))) |
187 | | - |
188 | | - ;; Print other page-level warnings |
189 | | - (when (seq other-warnings) |
190 | | - (println "\n⚠️ Page warnings:") |
191 | | - (doseq [w other-warnings] |
192 | | - ;; Prefer the :message field if available |
193 | | - (if-let [message (:message w)] |
194 | | - (println (format " - %s" message)) |
195 | | - ;; Fall back to type-specific formatting |
196 | | - (case (:type w) |
197 | | - :missing-template |
198 | | - (println (format " - Template '%s' not found for page '%s'" |
199 | | - (:template-name w) (:content-key w))) |
200 | | - |
201 | | - :defaulted-template |
202 | | - (println (format " - Page '%s' has no :template field, defaulting to '%s'" |
203 | | - (:content-key w) (:defaulted-to w))) |
204 | | - |
205 | | - :missing-content |
206 | | - (println (format " - No content for %s in %s" |
207 | | - (:content-key w) (:lang-code w))) |
208 | | - |
209 | | - :missing-key |
210 | | - (println (format " - Missing key '%s' in template %s (page: %s)" |
211 | | - (:key w) (:template w) (:page w))) |
212 | | - |
213 | | - :ambiguous-link |
214 | | - (println (format " - Ambiguous link '%s' exists as both page and section" |
215 | | - (:link-id w))) |
216 | | - |
217 | | - :missing-page |
218 | | - (let [stack-str (when (seq (:render-stack w)) |
219 | | - (->> (:render-stack w) |
220 | | - (map (fn [[type id]] |
221 | | - (str (name id) |
222 | | - (when (not= type :content) |
223 | | - (str " (" (name type) ")"))))) |
224 | | - (str/join " → ")))] |
225 | | - (if stack-str |
226 | | - (println (format " - Missing page '%s' in: %s" (:content-key w) stack-str)) |
227 | | - (println (format " - Missing page '%s'" (:content-key w))))) |
228 | | - |
229 | | - :invalid-render-spec |
230 | | - (let [stack-str (when (seq (:render-stack w)) |
231 | | - (->> (:render-stack w) |
232 | | - (map (fn [[type id]] |
233 | | - (str (name id) |
234 | | - (when (not= type :content) |
235 | | - (str " (" (name type) ")"))))) |
236 | | - (str/join " → ")))] |
237 | | - (println (format " - Invalid :eden/render spec with data=%s, template=%s%s" |
238 | | - (pr-str (:data-key w)) |
239 | | - (pr-str (:template-id w)) |
240 | | - (if stack-str (str " in: " stack-str) "")))) |
241 | | - |
242 | | - :missing-translation |
243 | | - nil ; Will be handled separately below |
244 | | - |
245 | | - ;; Default |
246 | | - (println (format " - %s: %s" (:type w) (pr-str w)))))))) |
247 | | - |
248 | | - ;; Print missing translations grouped by language |
249 | | - (let [missing-translations (filter #(= :missing-translation (:type %)) |
250 | | - (:page-warnings warnings))] |
251 | | - (when (seq missing-translations) |
252 | | - (let [by-lang (group-by :lang missing-translations) |
253 | | - config (get-in ctx [:results :load :config]) |
254 | | - root-path (:root-path config)] |
255 | | - (println "\n⚠️ Missing translations:") |
256 | | - (doseq [[lang translations] by-lang] |
257 | | - (println (format " Language: %s" (name lang))) |
258 | | - (doseq [t translations] |
259 | | - (println (format " - %s" (:key t)))) |
260 | | - ;; Show where we looked for translations |
261 | | - (when root-path |
262 | | - (let [strings-path (loader/translation-file-path root-path lang)] |
263 | | - (println (format " Expected in: %s" strings-path)))))))) |
264 | | - |
265 | | - ;; Print warnings (non-page warnings) |
266 | | - (when (seq (:missing-keys warnings)) |
267 | | - (println "\n⚠️ Missing template keys:") |
268 | | - (let [grouped (group-by :template (:missing-keys warnings))] |
269 | | - (doseq [[template keys] grouped] |
270 | | - (println (format " In %s:" template)) |
271 | | - (doseq [{:keys [key content-key]} keys] |
272 | | - (println (format " - %s (rendering %s)" key content-key)))))) |
273 | | - |
274 | | - (when (seq (:missing-pages warnings)) |
275 | | - (println "\n⚠️ Missing page references:") |
276 | | - (doseq [{:keys [page-id render-stack]} (:missing-pages warnings)] |
277 | | - (let [stack-str (when (seq render-stack) |
278 | | - (->> render-stack |
279 | | - (map (fn [[type id]] |
280 | | - (str (name id) |
281 | | - (when (not= type :content) |
282 | | - (str " (" (name type) ")"))))) |
283 | | - (str/join " → ")))] |
284 | | - (println) ;; Add blank line before each warning for better readability |
285 | | - (if stack-str |
286 | | - (println (format " - Page ID: %s\n Found in: %s" page-id stack-str)) |
287 | | - (println (format " - Page ID: %s" page-id)))))) |
288 | | - |
289 | | - ;; Print orphan content warning |
290 | | - (when-let [orphan-content (:orphan-content warnings)] |
291 | | - (when (seq orphan-content) |
292 | | - (println "\n📝 Found content files not linked from your site:") |
293 | | - (doseq [content-key (sort orphan-content)] |
294 | | - (println (format " - content/%s.edn (or .md)" (name content-key)))) |
295 | | - (println " To include them, add links from existing pages or add to :render-roots"))) |
296 | | - |
297 | | - ;; Print error if any |
298 | | - (when error |
| 18 | + (when-let [error (:error ctx)] |
299 | 19 | (println (format "\n❌ Build failed: %s" error)))) |
0 commit comments