elm-ssr 0.6.2 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/elm-ssr.mjs +75 -2
- package/elm-src/ElmSsr/Action.elm +19 -1
- package/elm-src/ElmSsr/Db/Dsl.elm +223 -0
- package/elm-src/ElmSsr/Loader.elm +33 -1
- package/elm-src/ElmSsr/Request/Decode.elm +478 -0
- package/elm-src/ElmSsr/Runtime.elm +3 -0
- package/lib/build.mjs +66 -1
- package/lib/query.mjs +464 -0
- package/lib/scaffold.mjs +156 -0
- package/package.json +1 -1
- package/src/app.ts +8 -2
- package/src/client-runtime/islands.ts +71 -4
- package/src/debugger.ts +535 -0
- package/src/effects.ts +8 -0
- package/src/request-handler.ts +38 -8
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
module ElmSsr.Request.Decode exposing
|
|
2
|
+
( Decoder, Error
|
|
3
|
+
, required, optional, optionalWithDefault
|
|
4
|
+
, string, int, float, bool
|
|
5
|
+
, validate, custom
|
|
6
|
+
, succeed, fail, map, map2, map3, map4, map5, map6, map7, map8, andThen
|
|
7
|
+
, decodeForm, decodeQuery, decodeParams, decodeRaw
|
|
8
|
+
, email, nonEmpty, minInt, maxInt, minFloat, maxFloat, minLength, maxLength
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
import ElmSsr.Route as Route exposing (Request)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
{-| Represents a single validation error on a field. -}
|
|
15
|
+
type alias Error =
|
|
16
|
+
{ field : String
|
|
17
|
+
, message : String
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
{-| A Request/Form decoder that compiles a validation result. -}
|
|
22
|
+
type Decoder a
|
|
23
|
+
= Decoder (List ( String, String ) -> Result (List Error) a)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
{-| A field decoder for a single, individual field's string representation. -}
|
|
27
|
+
type FieldDecoder a
|
|
28
|
+
= FieldDecoder (Maybe String -> Result String a)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
-- RUNNING DECODERS
|
|
32
|
+
|
|
33
|
+
{-| Decode from the request's raw form data (POST body). -}
|
|
34
|
+
decodeForm : Decoder a -> Request -> Result (List Error) a
|
|
35
|
+
decodeForm (Decoder dec) request =
|
|
36
|
+
dec request.formData
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
{-| Decode from the request's query string parameters (GET parameters). -}
|
|
40
|
+
decodeQuery : Decoder a -> Request -> Result (List Error) a
|
|
41
|
+
decodeQuery (Decoder dec) request =
|
|
42
|
+
dec request.query
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
{-| Decode from the dynamic route params. -}
|
|
46
|
+
decodeParams : Decoder a -> Request -> Result (List Error) a
|
|
47
|
+
decodeParams (Decoder dec) request =
|
|
48
|
+
dec request.params
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
{-| Decode directly from a raw key-value list. -}
|
|
52
|
+
decodeRaw : Decoder a -> List ( String, String ) -> Result (List Error) a
|
|
53
|
+
decodeRaw (Decoder dec) pairs =
|
|
54
|
+
dec pairs
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
-- BASE DECODERS
|
|
58
|
+
|
|
59
|
+
{-| Decodes a required string field. -}
|
|
60
|
+
string : FieldDecoder String
|
|
61
|
+
string =
|
|
62
|
+
FieldDecoder (\rawValue ->
|
|
63
|
+
case rawValue of
|
|
64
|
+
Just val ->
|
|
65
|
+
Ok val
|
|
66
|
+
|
|
67
|
+
Nothing ->
|
|
68
|
+
Err "Field is required"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
{-| Decodes an integer. -}
|
|
73
|
+
int : FieldDecoder Int
|
|
74
|
+
int =
|
|
75
|
+
FieldDecoder (\rawValue ->
|
|
76
|
+
case rawValue of
|
|
77
|
+
Just val ->
|
|
78
|
+
case String.toInt val of
|
|
79
|
+
Just num ->
|
|
80
|
+
Ok num
|
|
81
|
+
|
|
82
|
+
Nothing ->
|
|
83
|
+
Err "Must be a valid integer"
|
|
84
|
+
|
|
85
|
+
Nothing ->
|
|
86
|
+
Err "Field is required"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
{-| Decodes a float. -}
|
|
91
|
+
float : FieldDecoder Float
|
|
92
|
+
float =
|
|
93
|
+
FieldDecoder (\rawValue ->
|
|
94
|
+
case rawValue of
|
|
95
|
+
Just val ->
|
|
96
|
+
case String.toFloat val of
|
|
97
|
+
Just num ->
|
|
98
|
+
Ok num
|
|
99
|
+
|
|
100
|
+
Nothing ->
|
|
101
|
+
Err "Must be a valid number"
|
|
102
|
+
|
|
103
|
+
Nothing ->
|
|
104
|
+
Err "Field is required"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
{-| Decodes a boolean checkbox. Returns `True` for `"true"`, `"on"`, or presence.
|
|
109
|
+
Returns `False` otherwise or if omitted.
|
|
110
|
+
-}
|
|
111
|
+
bool : FieldDecoder Bool
|
|
112
|
+
bool =
|
|
113
|
+
FieldDecoder (\rawValue ->
|
|
114
|
+
case rawValue of
|
|
115
|
+
Just "true" ->
|
|
116
|
+
Ok True
|
|
117
|
+
|
|
118
|
+
Just "false" ->
|
|
119
|
+
Ok False
|
|
120
|
+
|
|
121
|
+
Just "on" ->
|
|
122
|
+
Ok True
|
|
123
|
+
|
|
124
|
+
Just "" ->
|
|
125
|
+
Ok False
|
|
126
|
+
|
|
127
|
+
Just _ ->
|
|
128
|
+
Ok False
|
|
129
|
+
|
|
130
|
+
Nothing ->
|
|
131
|
+
Ok False
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
-- PIPELINE HELPERS
|
|
136
|
+
|
|
137
|
+
{-| Decodes a required field. If missing or invalid, accumulates the error. -}
|
|
138
|
+
required : String -> FieldDecoder b -> Decoder (b -> a) -> Decoder a
|
|
139
|
+
required name (FieldDecoder fieldDec) (Decoder decFn) =
|
|
140
|
+
Decoder (\input ->
|
|
141
|
+
let
|
|
142
|
+
rawValue =
|
|
143
|
+
lookup name input
|
|
144
|
+
|
|
145
|
+
resFn =
|
|
146
|
+
decFn input
|
|
147
|
+
|
|
148
|
+
resVal =
|
|
149
|
+
case fieldDec rawValue of
|
|
150
|
+
Ok val ->
|
|
151
|
+
Ok val
|
|
152
|
+
|
|
153
|
+
Err msg ->
|
|
154
|
+
Err [ { field = name, message = msg } ]
|
|
155
|
+
in
|
|
156
|
+
combine resFn resVal
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
{-| Decodes an optional field, defaulting to `Nothing` if missing or empty. -}
|
|
161
|
+
optional : String -> FieldDecoder b -> Decoder (Maybe b -> a) -> Decoder a
|
|
162
|
+
optional name (FieldDecoder fieldDec) (Decoder decFn) =
|
|
163
|
+
Decoder (\input ->
|
|
164
|
+
let
|
|
165
|
+
rawValue =
|
|
166
|
+
lookup name input
|
|
167
|
+
|
|
168
|
+
resFn =
|
|
169
|
+
decFn input
|
|
170
|
+
|
|
171
|
+
resVal =
|
|
172
|
+
case rawValue of
|
|
173
|
+
Nothing ->
|
|
174
|
+
Ok Nothing
|
|
175
|
+
|
|
176
|
+
Just "" ->
|
|
177
|
+
Ok Nothing
|
|
178
|
+
|
|
179
|
+
Just val ->
|
|
180
|
+
case fieldDec (Just val) of
|
|
181
|
+
Ok decoded ->
|
|
182
|
+
Ok (Just decoded)
|
|
183
|
+
|
|
184
|
+
Err msg ->
|
|
185
|
+
Err [ { field = name, message = msg } ]
|
|
186
|
+
in
|
|
187
|
+
combine resFn resVal
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
{-| Decodes an optional field, defaulting to the provided value if missing or empty. -}
|
|
192
|
+
optionalWithDefault : String -> b -> FieldDecoder b -> Decoder (b -> a) -> Decoder a
|
|
193
|
+
optionalWithDefault name defaultVal (FieldDecoder fieldDec) (Decoder decFn) =
|
|
194
|
+
Decoder (\input ->
|
|
195
|
+
let
|
|
196
|
+
rawValue =
|
|
197
|
+
lookup name input
|
|
198
|
+
|
|
199
|
+
resFn =
|
|
200
|
+
decFn input
|
|
201
|
+
|
|
202
|
+
resVal =
|
|
203
|
+
case rawValue of
|
|
204
|
+
Nothing ->
|
|
205
|
+
Ok defaultVal
|
|
206
|
+
|
|
207
|
+
Just "" ->
|
|
208
|
+
Ok defaultVal
|
|
209
|
+
|
|
210
|
+
Just val ->
|
|
211
|
+
case fieldDec (Just val) of
|
|
212
|
+
Ok decoded ->
|
|
213
|
+
Ok decoded
|
|
214
|
+
|
|
215
|
+
Err msg ->
|
|
216
|
+
Err [ { field = name, message = msg } ]
|
|
217
|
+
in
|
|
218
|
+
combine resFn resVal
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
-- APPLICATIVE COMBINATORS
|
|
223
|
+
|
|
224
|
+
{-| A decoder that always succeeds with the given value. -}
|
|
225
|
+
succeed : a -> Decoder a
|
|
226
|
+
succeed val =
|
|
227
|
+
Decoder (\_ -> Ok val)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
{-| A decoder that always fails with a single general validation error. -}
|
|
231
|
+
fail : String -> String -> Decoder a
|
|
232
|
+
fail fieldName msg =
|
|
233
|
+
Decoder (\_ -> Err [ { field = fieldName, message = msg } ])
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
{-| Transform a decoded value. -}
|
|
237
|
+
map : (a -> b) -> Decoder a -> Decoder b
|
|
238
|
+
map f (Decoder dec) =
|
|
239
|
+
Decoder (\input -> Result.map f (dec input))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
{-| Apply a function inside a decoder to a value inside a decoder. -}
|
|
243
|
+
apply : Decoder a -> Decoder (a -> b) -> Decoder b
|
|
244
|
+
apply (Decoder decVal) (Decoder decFn) =
|
|
245
|
+
Decoder (\input ->
|
|
246
|
+
combine (decFn input) (decVal input)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
{-| Map over 2 decoders, accumulating errors. -}
|
|
251
|
+
map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c
|
|
252
|
+
map2 f decA decB =
|
|
253
|
+
succeed f
|
|
254
|
+
|> apply decA
|
|
255
|
+
|> apply decB
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
{-| Map over 3 decoders, accumulating errors. -}
|
|
259
|
+
map3 : (a -> b -> c -> d) -> Decoder a -> Decoder b -> Decoder c -> Decoder d
|
|
260
|
+
map3 f decA decB decC =
|
|
261
|
+
succeed f
|
|
262
|
+
|> apply decA
|
|
263
|
+
|> apply decB
|
|
264
|
+
|> apply decC
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
{-| Map over 4 decoders, accumulating errors. -}
|
|
268
|
+
map4 : (a -> b -> c -> d -> e) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e
|
|
269
|
+
map4 f decA decB decC decD =
|
|
270
|
+
succeed f
|
|
271
|
+
|> apply decA
|
|
272
|
+
|> apply decB
|
|
273
|
+
|> apply decC
|
|
274
|
+
|> apply decD
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
{-| Map over 5 decoders, accumulating errors. -}
|
|
278
|
+
map5 : (a -> b -> c -> d -> e -> f) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f
|
|
279
|
+
map5 f decA decB decC decD decE =
|
|
280
|
+
succeed f
|
|
281
|
+
|> apply decA
|
|
282
|
+
|> apply decB
|
|
283
|
+
|> apply decC
|
|
284
|
+
|> apply decD
|
|
285
|
+
|> apply decE
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
{-| Map over 6 decoders, accumulating errors. -}
|
|
289
|
+
map6 : (a -> b -> c -> d -> e -> f -> g) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g
|
|
290
|
+
map6 f decA decB decC decD decE decF =
|
|
291
|
+
succeed f
|
|
292
|
+
|> apply decA
|
|
293
|
+
|> apply decB
|
|
294
|
+
|> apply decC
|
|
295
|
+
|> apply decD
|
|
296
|
+
|> apply decE
|
|
297
|
+
|> apply decF
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
{-| Map over 7 decoders, accumulating errors. -}
|
|
301
|
+
map7 : (a -> b -> c -> d -> e -> f -> g -> h) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h
|
|
302
|
+
map7 f decA decB decC decD decE decF decG =
|
|
303
|
+
succeed f
|
|
304
|
+
|> apply decA
|
|
305
|
+
|> apply decB
|
|
306
|
+
|> apply decC
|
|
307
|
+
|> apply decD
|
|
308
|
+
|> apply decE
|
|
309
|
+
|> apply decF
|
|
310
|
+
|> apply decG
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
{-| Map over 8 decoders, accumulating errors. -}
|
|
314
|
+
map8 : (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h -> Decoder i
|
|
315
|
+
map8 f decA decB decC decD decE decF decG decH =
|
|
316
|
+
succeed f
|
|
317
|
+
|> apply decA
|
|
318
|
+
|> apply decB
|
|
319
|
+
|> apply decC
|
|
320
|
+
|> apply decD
|
|
321
|
+
|> apply decE
|
|
322
|
+
|> apply decF
|
|
323
|
+
|> apply decG
|
|
324
|
+
|> apply decH
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
{-| Chain two decoders, short-circuiting on failure. -}
|
|
328
|
+
andThen : (a -> Decoder b) -> Decoder a -> Decoder b
|
|
329
|
+
andThen f (Decoder decA) =
|
|
330
|
+
Decoder (\input ->
|
|
331
|
+
case decA input of
|
|
332
|
+
Ok a ->
|
|
333
|
+
let
|
|
334
|
+
(Decoder decB) =
|
|
335
|
+
f a
|
|
336
|
+
in
|
|
337
|
+
decB input
|
|
338
|
+
|
|
339
|
+
Err errs ->
|
|
340
|
+
Err errs
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
-- VALIDATION & CUSTOM TRANSFORMS
|
|
345
|
+
|
|
346
|
+
{-| Run a custom validation function on a field. -}
|
|
347
|
+
validate : (a -> Result String a) -> FieldDecoder a -> FieldDecoder a
|
|
348
|
+
validate validator (FieldDecoder fieldDec) =
|
|
349
|
+
FieldDecoder (\rawValue ->
|
|
350
|
+
fieldDec rawValue
|
|
351
|
+
|> Result.andThen validator
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
{-| Map a value to a new type, allowing custom validation errors. -}
|
|
356
|
+
custom : (a -> Result String b) -> FieldDecoder a -> FieldDecoder b
|
|
357
|
+
custom f (FieldDecoder fieldDec) =
|
|
358
|
+
FieldDecoder (\rawValue ->
|
|
359
|
+
fieldDec rawValue
|
|
360
|
+
|> Result.andThen f
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
-- COMMON VALIDATORS
|
|
365
|
+
|
|
366
|
+
{-| Validates a string is a standard email address. -}
|
|
367
|
+
email : String -> Result String String
|
|
368
|
+
email val =
|
|
369
|
+
let
|
|
370
|
+
parts =
|
|
371
|
+
String.split "@" val
|
|
372
|
+
in
|
|
373
|
+
case parts of
|
|
374
|
+
[ _, domain ] ->
|
|
375
|
+
if String.contains "." domain then
|
|
376
|
+
Ok val
|
|
377
|
+
|
|
378
|
+
else
|
|
379
|
+
Err "Invalid email address"
|
|
380
|
+
|
|
381
|
+
_ ->
|
|
382
|
+
Err "Invalid email address"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
{-| Validates a string is not empty or only whitespace. -}
|
|
386
|
+
nonEmpty : String -> Result String String
|
|
387
|
+
nonEmpty val =
|
|
388
|
+
if String.isEmpty (String.trim val) then
|
|
389
|
+
Err "Cannot be empty"
|
|
390
|
+
|
|
391
|
+
else
|
|
392
|
+
Ok val
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
{-| Validates a minimum integer value. -}
|
|
396
|
+
minInt : Int -> Int -> Result String Int
|
|
397
|
+
minInt min val =
|
|
398
|
+
if val >= min then
|
|
399
|
+
Ok val
|
|
400
|
+
|
|
401
|
+
else
|
|
402
|
+
Err ("Must be at least " ++ String.fromInt min)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
{-| Validates a maximum integer value. -}
|
|
406
|
+
maxInt : Int -> Int -> Result String Int
|
|
407
|
+
maxInt max val =
|
|
408
|
+
if val <= max then
|
|
409
|
+
Ok val
|
|
410
|
+
|
|
411
|
+
else
|
|
412
|
+
Err ("Must be at most " ++ String.fromInt max)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
{-| Validates a minimum float value. -}
|
|
416
|
+
minFloat : Float -> Float -> Result String Float
|
|
417
|
+
minFloat min val =
|
|
418
|
+
if val >= min then
|
|
419
|
+
Ok val
|
|
420
|
+
|
|
421
|
+
else
|
|
422
|
+
Err ("Must be at least " ++ String.fromFloat min)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
{-| Validates a maximum float value. -}
|
|
426
|
+
maxFloat : Float -> Float -> Result String Float
|
|
427
|
+
maxFloat max val =
|
|
428
|
+
if val <= max then
|
|
429
|
+
Ok val
|
|
430
|
+
|
|
431
|
+
else
|
|
432
|
+
Err ("Must be at most " ++ String.fromFloat max)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
{-| Validates a minimum string length. -}
|
|
436
|
+
minLength : Int -> String -> Result String String
|
|
437
|
+
minLength len val =
|
|
438
|
+
if String.length val >= len then
|
|
439
|
+
Ok val
|
|
440
|
+
|
|
441
|
+
else
|
|
442
|
+
Err ("Must be at least " ++ String.fromInt len ++ " characters")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
{-| Validates a maximum string length. -}
|
|
446
|
+
maxLength : Int -> String -> Result String String
|
|
447
|
+
maxLength len val =
|
|
448
|
+
if String.length val <= len then
|
|
449
|
+
Ok val
|
|
450
|
+
|
|
451
|
+
else
|
|
452
|
+
Err ("Must be at most " ++ String.fromInt len ++ " characters")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
-- INTERNAL HELPERS
|
|
456
|
+
|
|
457
|
+
lookup : String -> List ( String, String ) -> Maybe String
|
|
458
|
+
lookup key pairs =
|
|
459
|
+
pairs
|
|
460
|
+
|> List.filter (\( name, _ ) -> name == key)
|
|
461
|
+
|> List.head
|
|
462
|
+
|> Maybe.map Tuple.second
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
combine : Result (List Error) (a -> b) -> Result (List Error) a -> Result (List Error) b
|
|
466
|
+
combine resFn resVal =
|
|
467
|
+
case ( resFn, resVal ) of
|
|
468
|
+
( Ok fn, Ok val ) ->
|
|
469
|
+
Ok (fn val)
|
|
470
|
+
|
|
471
|
+
( Err errsFn, Err errsVal ) ->
|
|
472
|
+
Err (errsFn ++ errsVal)
|
|
473
|
+
|
|
474
|
+
( Err errs, Ok _ ) ->
|
|
475
|
+
Err errs
|
|
476
|
+
|
|
477
|
+
( Ok _, Err errs ) ->
|
|
478
|
+
Err errs
|
|
@@ -122,6 +122,9 @@ advance config loader =
|
|
|
122
122
|
Loader.Errored status message ->
|
|
123
123
|
( Aborted status message, renderError config status message )
|
|
124
124
|
|
|
125
|
+
Loader.Moved url ->
|
|
126
|
+
( Rendered, config.ports.rendered (Action.encodeStep [] (always Json.null) (Action.Moved url)) )
|
|
127
|
+
|
|
125
128
|
Loader.Await effect continue ->
|
|
126
129
|
( LoadingPage continue, config.ports.effectRequest (Loader.encodeEffect effect) )
|
|
127
130
|
|
package/lib/build.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, rm, writeFile, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, relative, resolve } from "node:path";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
4
8
|
|
|
5
9
|
// This lib is part of elm-ssr. It handles generating Main.elm and
|
|
6
10
|
// compiling the route apps and island bundles.
|
|
@@ -199,10 +203,71 @@ main =
|
|
|
199
203
|
await rm(rawOutputPath, { force: true });
|
|
200
204
|
};
|
|
201
205
|
|
|
206
|
+
const findLocalTailwind = async (startDir) => {
|
|
207
|
+
let current = resolve(startDir);
|
|
208
|
+
while (true) {
|
|
209
|
+
const candidate = resolve(current, "node_modules", ".bin", "tailwindcss");
|
|
210
|
+
try {
|
|
211
|
+
await stat(candidate);
|
|
212
|
+
return candidate;
|
|
213
|
+
} catch {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
const parent = dirname(current);
|
|
217
|
+
if (parent === current) break;
|
|
218
|
+
current = parent;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const compileStylesheet = async ({ inputPath, outputPath, appConfig, appRoot }) => {
|
|
224
|
+
try {
|
|
225
|
+
await stat(inputPath);
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let css = "";
|
|
231
|
+
const isTailwindEnabled = appConfig.tailwind === true;
|
|
232
|
+
|
|
233
|
+
if (isTailwindEnabled) {
|
|
234
|
+
try {
|
|
235
|
+
console.log(`[elm-ssr] Compiling Tailwind CSS for app "${appConfig.name}"...`);
|
|
236
|
+
const contentGlob = "src/**/*.elm,src/**/*.ts,src/**/*.js";
|
|
237
|
+
|
|
238
|
+
let tailwindBin = await findLocalTailwind(rootPath);
|
|
239
|
+
if (!tailwindBin) {
|
|
240
|
+
tailwindBin = await findLocalTailwind(cliRoot);
|
|
241
|
+
}
|
|
242
|
+
const binCommand = tailwindBin ? tailwindBin : "npx --yes tailwindcss";
|
|
243
|
+
|
|
244
|
+
const command = `${binCommand} -i ${inputPath} --content "${contentGlob}" --minify`;
|
|
245
|
+
const { stdout } = await execAsync(command, { cwd: appRoot });
|
|
246
|
+
css = stdout.toString().trim();
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.warn(`[elm-ssr] Tailwind CSS compilation failed. Falling back to plain CSS read. Error:`, err.message);
|
|
249
|
+
const raw = await readFile(inputPath, "utf8");
|
|
250
|
+
css = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
const raw = await readFile(inputPath, "utf8");
|
|
254
|
+
css = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const tsContent = `export const stylesheet = \`${css.replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`;\n`;
|
|
258
|
+
await writeFile(outputPath, tsContent, "utf8");
|
|
259
|
+
};
|
|
260
|
+
|
|
202
261
|
await mkdir(elmHome, { recursive: true });
|
|
203
262
|
|
|
204
263
|
for (const appConfig of config.apps) {
|
|
205
264
|
const exampleRoot = resolve(rootPath, appConfig.root);
|
|
265
|
+
await compileStylesheet({
|
|
266
|
+
inputPath: resolve(exampleRoot, "src", "app.css"),
|
|
267
|
+
outputPath: resolve(exampleRoot, "styles.ts"),
|
|
268
|
+
appConfig,
|
|
269
|
+
appRoot: exampleRoot
|
|
270
|
+
});
|
|
206
271
|
const namespace = appConfig.module;
|
|
207
272
|
const namespacePath = resolve(exampleRoot, "src", moduleToPath(namespace));
|
|
208
273
|
const routes = await collectRoutes(namespace, resolve(namespacePath, "Routes"));
|