elm-ssr 0.1.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/README.md +67 -0
- package/bin/elm-ssr.mjs +102 -0
- package/elm-src/ElmSsr/Action.elm +210 -0
- package/elm-src/ElmSsr/Document/Encode.elm +83 -0
- package/elm-src/ElmSsr/Document/Events.elm +125 -0
- package/elm-src/ElmSsr/Document.elm +26 -0
- package/elm-src/ElmSsr/Html/Attributes.elm +344 -0
- package/elm-src/ElmSsr/Html/Events.elm +95 -0
- package/elm-src/ElmSsr/Html.elm +706 -0
- package/elm-src/ElmSsr/Island/Shared.elm +38 -0
- package/elm-src/ElmSsr/Island.elm +49 -0
- package/elm-src/ElmSsr/Loader.elm +297 -0
- package/elm-src/ElmSsr/Page.elm +102 -0
- package/elm-src/ElmSsr/Route.elm +136 -0
- package/elm-src/ElmSsr/Runtime.elm +170 -0
- package/elm-src/ElmSsr/Svg/Attributes.elm +1208 -0
- package/elm-src/ElmSsr/Svg.elm +309 -0
- package/lib/build.mjs +256 -0
- package/lib/migrate.mjs +146 -0
- package/lib/scaffold.mjs +472 -0
- package/lib/workspace.mjs +21 -0
- package/package.json +60 -0
- package/src/app.ts +74 -0
- package/src/backends.ts +116 -0
- package/src/client-runtime/islands.ts +247 -0
- package/src/effects.ts +267 -0
- package/src/http.ts +86 -0
- package/src/middleware.ts +104 -0
- package/src/migrations.ts +225 -0
- package/src/protocol.ts +119 -0
- package/src/render.ts +111 -0
- package/src/request-handler.ts +208 -0
- package/src/response-headers.ts +18 -0
- package/src/serialize.ts +47 -0
- package/src/tasks.ts +139 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
module ElmSsr.Svg exposing
|
|
2
|
+
( Svg
|
|
3
|
+
, svg, a, circle, clipPath, defs, desc, ellipse, feBlend, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap, feDistantLight, feFlood, feFuncA, feFuncB, feFuncG, feFuncR, feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset, fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence, filter, foreignObject, g, image, line, linearGradient, marker, mask, metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect, stop, switch, symbol, text, text_, textPath, title, tspan, use, view
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
{-| SVG elements for ElmSsr. Mirrors `elm/svg`.
|
|
7
|
+
|
|
8
|
+
@docs Svg
|
|
9
|
+
@docs svg, a, circle, clipPath, defs, desc, ellipse, feBlend, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap, feDistantLight, feFlood, feFuncA, feFuncB, feFuncG, feFuncR, feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset, fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence, filter, foreignObject, g, image, line, linearGradient, marker, mask, metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect, stop, switch, symbol, text, text_, textPath, title, tspan, use, view
|
|
10
|
+
-}
|
|
11
|
+
|
|
12
|
+
import ElmSsr.Html as Html exposing (Attribute, Node)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
{-| The SVG node type. -}
|
|
16
|
+
type alias Svg msg =
|
|
17
|
+
Node msg
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
element : String -> List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
21
|
+
element =
|
|
22
|
+
Html.element
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
svg : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
26
|
+
svg =
|
|
27
|
+
element "svg"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
a : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
31
|
+
a =
|
|
32
|
+
element "a"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
circle : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
36
|
+
circle =
|
|
37
|
+
element "circle"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
clipPath : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
41
|
+
clipPath =
|
|
42
|
+
element "clipPath"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
defs : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
46
|
+
defs =
|
|
47
|
+
element "defs"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
desc : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
51
|
+
desc =
|
|
52
|
+
element "desc"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
ellipse : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
56
|
+
ellipse =
|
|
57
|
+
element "ellipse"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
feBlend : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
61
|
+
feBlend =
|
|
62
|
+
element "feBlend"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
feColorMatrix : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
66
|
+
feColorMatrix =
|
|
67
|
+
element "feColorMatrix"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
feComponentTransfer : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
71
|
+
feComponentTransfer =
|
|
72
|
+
element "feComponentTransfer"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
feComposite : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
76
|
+
feComposite =
|
|
77
|
+
element "feComposite"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
feConvolveMatrix : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
81
|
+
feConvolveMatrix =
|
|
82
|
+
element "feConvolveMatrix"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
feDiffuseLighting : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
86
|
+
feDiffuseLighting =
|
|
87
|
+
element "feDiffuseLighting"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
feDisplacementMap : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
91
|
+
feDisplacementMap =
|
|
92
|
+
element "feDisplacementMap"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
feDistantLight : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
96
|
+
feDistantLight =
|
|
97
|
+
element "feDistantLight"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
feFlood : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
101
|
+
feFlood =
|
|
102
|
+
element "feFlood"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
feFuncA : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
106
|
+
feFuncA =
|
|
107
|
+
element "feFuncA"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
feFuncB : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
111
|
+
feFuncB =
|
|
112
|
+
element "feFuncB"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
feFuncG : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
116
|
+
feFuncG =
|
|
117
|
+
element "feFuncG"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
feFuncR : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
121
|
+
feFuncR =
|
|
122
|
+
element "feFuncR"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
feGaussianBlur : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
126
|
+
feGaussianBlur =
|
|
127
|
+
element "feGaussianBlur"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
feImage : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
131
|
+
feImage =
|
|
132
|
+
element "feImage"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
feMerge : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
136
|
+
feMerge =
|
|
137
|
+
element "feMerge"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
feMergeNode : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
141
|
+
feMergeNode =
|
|
142
|
+
element "feMergeNode"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
feMorphology : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
146
|
+
feMorphology =
|
|
147
|
+
element "feMorphology"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
feOffset : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
151
|
+
feOffset =
|
|
152
|
+
element "feOffset"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
fePointLight : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
156
|
+
fePointLight =
|
|
157
|
+
element "fePointLight"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
feSpecularLighting : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
161
|
+
feSpecularLighting =
|
|
162
|
+
element "feSpecularLighting"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
feSpotLight : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
166
|
+
feSpotLight =
|
|
167
|
+
element "feSpotLight"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
feTile : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
171
|
+
feTile =
|
|
172
|
+
element "feTile"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
feTurbulence : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
176
|
+
feTurbulence =
|
|
177
|
+
element "feTurbulence"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
filter : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
181
|
+
filter =
|
|
182
|
+
element "filter"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
foreignObject : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
186
|
+
foreignObject =
|
|
187
|
+
element "foreignObject"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
g : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
191
|
+
g =
|
|
192
|
+
element "g"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
image : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
196
|
+
image =
|
|
197
|
+
element "image"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
line : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
201
|
+
line =
|
|
202
|
+
element "line"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
linearGradient : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
206
|
+
linearGradient =
|
|
207
|
+
element "linearGradient"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
marker : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
211
|
+
marker =
|
|
212
|
+
element "marker"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
mask : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
216
|
+
mask =
|
|
217
|
+
element "mask"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
metadata : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
221
|
+
metadata =
|
|
222
|
+
element "metadata"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
mpath : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
226
|
+
mpath =
|
|
227
|
+
element "mpath"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
path : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
231
|
+
path =
|
|
232
|
+
element "path"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
pattern : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
236
|
+
pattern =
|
|
237
|
+
element "pattern"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
polygon : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
241
|
+
polygon =
|
|
242
|
+
element "polygon"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
polyline : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
246
|
+
polyline =
|
|
247
|
+
element "polyline"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
radialGradient : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
251
|
+
radialGradient =
|
|
252
|
+
element "radialGradient"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
rect : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
256
|
+
rect =
|
|
257
|
+
element "rect"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
stop : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
261
|
+
stop =
|
|
262
|
+
element "stop"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
switch : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
266
|
+
switch =
|
|
267
|
+
element "switch"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
symbol : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
271
|
+
symbol =
|
|
272
|
+
element "symbol"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
{-| A text node, e.g. the content of a `<text>` element. Mirrors `Svg.text`. -}
|
|
276
|
+
text : String -> Svg msg
|
|
277
|
+
text =
|
|
278
|
+
Html.text
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
{-| The `<text>` element. Mirrors `Svg.text_`. -}
|
|
282
|
+
text_ : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
283
|
+
text_ =
|
|
284
|
+
element "text"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
textPath : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
288
|
+
textPath =
|
|
289
|
+
element "textPath"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
title : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
293
|
+
title =
|
|
294
|
+
element "title"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
tspan : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
298
|
+
tspan =
|
|
299
|
+
element "tspan"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
use : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
303
|
+
use =
|
|
304
|
+
element "use"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
view : List (Attribute msg) -> List (Svg msg) -> Svg msg
|
|
308
|
+
view =
|
|
309
|
+
element "view"
|
package/lib/build.mjs
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
4
|
+
|
|
5
|
+
// This lib is part of elm-ssr. It handles generating Main.elm and
|
|
6
|
+
// compiling the route apps and island bundles.
|
|
7
|
+
|
|
8
|
+
export const build = async ({ rootPath, config }) => {
|
|
9
|
+
const cliRoot = resolve(new URL("..", import.meta.url).pathname);
|
|
10
|
+
|
|
11
|
+
// Try to find elm in local node_modules first (for monorepo/dev), then fall back to global 'elm'
|
|
12
|
+
let elmBinary = "elm";
|
|
13
|
+
try {
|
|
14
|
+
const localElm = resolve(rootPath, "node_modules", ".bin", "elm");
|
|
15
|
+
await readFile(localElm); // Check if exists
|
|
16
|
+
elmBinary = localElm;
|
|
17
|
+
} catch {
|
|
18
|
+
// fall back to "elm" in PATH
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const elmHome = resolve(rootPath, ".elm-home");
|
|
22
|
+
|
|
23
|
+
const moduleToPath = (moduleName) => moduleName.split(".").join("/");
|
|
24
|
+
|
|
25
|
+
const walkElmFiles = async (dir) => {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const full = resolve(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
files.push(...(await walkElmFiles(full)));
|
|
37
|
+
} else if (entry.isFile() && entry.name.endsWith(".elm")) {
|
|
38
|
+
files.push(full);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const parseSegment = (part) => {
|
|
45
|
+
if (part.endsWith("_")) {
|
|
46
|
+
const name = part.slice(0, -1).toLowerCase();
|
|
47
|
+
return { kind: "dynamic", name, binding: `p_${name}` };
|
|
48
|
+
}
|
|
49
|
+
return { kind: "static", value: part.toLowerCase() };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const segmentPattern = (segments) =>
|
|
53
|
+
segments.length === 0
|
|
54
|
+
? "[]"
|
|
55
|
+
: `[ ${segments.map((segment) => (segment.kind === "dynamic" ? segment.binding : `"${segment.value}"`)).join(", ")} ]`;
|
|
56
|
+
|
|
57
|
+
const dynamicCount = (segments) => segments.filter((segment) => segment.kind === "dynamic").length;
|
|
58
|
+
|
|
59
|
+
const requestExpression = (segments) => {
|
|
60
|
+
const dynamic = segments.filter((segment) => segment.kind === "dynamic");
|
|
61
|
+
if (dynamic.length === 0) return "request";
|
|
62
|
+
const pairs = dynamic.map((segment) => `( "${segment.name}", ${segment.binding} )`).join(", ");
|
|
63
|
+
return `{ request | params = [ ${pairs} ] }`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const collectRoutes = async (namespace, routesDir) => {
|
|
67
|
+
const files = (await walkElmFiles(routesDir)).sort();
|
|
68
|
+
return files.map((file) => {
|
|
69
|
+
const parts = relative(routesDir, file).replace(/\.elm$/, "").split("/");
|
|
70
|
+
const last = parts[parts.length - 1];
|
|
71
|
+
const isFallback = parts.length === 1 && last === "NotFound";
|
|
72
|
+
const segmentParts = last === "Index" ? parts.slice(0, -1) : parts;
|
|
73
|
+
return {
|
|
74
|
+
moduleName: `${namespace}.Routes.${parts.join(".")}`,
|
|
75
|
+
alias: parts.join("_"),
|
|
76
|
+
isFallback,
|
|
77
|
+
segments: segmentParts.map(parseSegment)
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const collectIslands = async (namespace, islandsDir) => {
|
|
83
|
+
const files = (await walkElmFiles(islandsDir)).sort();
|
|
84
|
+
return Promise.all(
|
|
85
|
+
files.map(async (file) => {
|
|
86
|
+
const parts = relative(islandsDir, file).replace(/\.elm$/, "").split("/");
|
|
87
|
+
const alias = parts.join("_");
|
|
88
|
+
const key = alias;
|
|
89
|
+
const moduleName = `${namespace}.Islands.${parts.join(".")}`;
|
|
90
|
+
const source = await readFile(file, "utf8");
|
|
91
|
+
const declared = source.match(/\.embed\s+"([^"]+)"/);
|
|
92
|
+
if (!declared) {
|
|
93
|
+
throw new Error(`Island ${moduleName} must expose an embed defined with Island.embed "${key}" { .. }.`);
|
|
94
|
+
}
|
|
95
|
+
if (declared[1] !== key) {
|
|
96
|
+
throw new Error(`Island ${moduleName} declares Island.embed "${declared[1]}" but its module path requires "${key}".`);
|
|
97
|
+
}
|
|
98
|
+
return { moduleName, alias, key, file };
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const generateMain = (routes) => {
|
|
104
|
+
const fallback = routes.find((route) => route.isFallback);
|
|
105
|
+
const matched = routes
|
|
106
|
+
.filter((route) => !route.isFallback)
|
|
107
|
+
.sort((left, right) => dynamicCount(left.segments) - dynamicCount(right.segments));
|
|
108
|
+
|
|
109
|
+
const imports = [
|
|
110
|
+
"import ElmSsr.Action as Action exposing (Action)",
|
|
111
|
+
"import ElmSsr.Document exposing (Document)",
|
|
112
|
+
"import ElmSsr.Loader as Loader exposing (Loader)",
|
|
113
|
+
"import ElmSsr.Route as Route exposing (Request)",
|
|
114
|
+
"import ElmSsr.Runtime as Runtime",
|
|
115
|
+
...routes.map((route) => `import ${route.moduleName} as ${route.alias}`),
|
|
116
|
+
"import Json.Decode as Decode",
|
|
117
|
+
"import Json.Encode as Encode"
|
|
118
|
+
].sort().join("\n");
|
|
119
|
+
|
|
120
|
+
const routerArm = (route) => {
|
|
121
|
+
const pattern = segmentPattern(route.segments);
|
|
122
|
+
const request = requestExpression(route.segments);
|
|
123
|
+
return ` ${pattern} ->\n ${route.alias}.page ${request === "request" ? "request" : `(${request})`}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const actionArm = (route) => {
|
|
127
|
+
const pattern = segmentPattern(route.segments);
|
|
128
|
+
const request = requestExpression(route.segments);
|
|
129
|
+
return ` ${pattern} ->\n ${route.alias}.action ${request === "request" ? "request" : `(${request})`}`;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const fallbackArm = fallback ? ` _ ->\n ${fallback.alias}.page request` : ` _ ->\n Loader.fail 404 "Not found"`;
|
|
133
|
+
const actionFallbackArm = fallback ? ` _ ->\n ${fallback.alias}.action request` : ` _ ->\n Action.fail 404 "Not found"`;
|
|
134
|
+
|
|
135
|
+
return `port module Main exposing (main)
|
|
136
|
+
-- Generated by elm-ssr. Do not edit.
|
|
137
|
+
${imports}
|
|
138
|
+
|
|
139
|
+
port effectRequest : Encode.Value -> Cmd msg
|
|
140
|
+
port effectResult : (Decode.Value -> msg) -> Sub msg
|
|
141
|
+
port rendered : Encode.Value -> Cmd msg
|
|
142
|
+
port start : (Decode.Value -> msg) -> Sub msg
|
|
143
|
+
|
|
144
|
+
router : Request -> Loader (Document Never)
|
|
145
|
+
router request =
|
|
146
|
+
case Route.segments request of
|
|
147
|
+
${[...matched.map(routerArm), fallbackArm].join("\n\n")}
|
|
148
|
+
|
|
149
|
+
action : Request -> Action (Document Never)
|
|
150
|
+
action request =
|
|
151
|
+
case Route.segments request of
|
|
152
|
+
${[...matched.map(actionArm), actionFallbackArm].join("\n\n")}
|
|
153
|
+
|
|
154
|
+
main : Program Decode.Value Runtime.State Runtime.Msg
|
|
155
|
+
main =
|
|
156
|
+
Runtime.program
|
|
157
|
+
{ router = router
|
|
158
|
+
, action = action
|
|
159
|
+
, ports = { effectRequest = effectRequest, effectResult = effectResult, rendered = rendered, start = start }
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const generateIslandsManifestModule = (islands) => {
|
|
165
|
+
if (islands.length === 0) return "export const islands = {};\nexport const bundleSource = \"\";\n";
|
|
166
|
+
const entries = islands.map((island) => ` "${island.key}": { module: "${island.moduleName}" }`).join(",\n");
|
|
167
|
+
return `import bundleSource from "./islands-source";\nexport const islands = {\n${entries}\n};\nexport { bundleSource };\n`;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const compileEntrypoint = async ({ cwd, entrypoint, outputDir, outputName }) => {
|
|
171
|
+
const rawOutputPath = resolve(outputDir, `${outputName}.raw.js`);
|
|
172
|
+
const finalOutputPath = resolve(outputDir, `${outputName}.mjs`);
|
|
173
|
+
const sourceModulePath = resolve(outputDir, `${outputName}-source.ts`);
|
|
174
|
+
const entrypoints = Array.isArray(entrypoint) ? entrypoint : [entrypoint];
|
|
175
|
+
|
|
176
|
+
await mkdir(dirname(rawOutputPath), { recursive: true });
|
|
177
|
+
|
|
178
|
+
const buildChild = Bun.spawn(
|
|
179
|
+
[elmBinary, "make", ...entrypoints, "--optimize", "--output", rawOutputPath],
|
|
180
|
+
{
|
|
181
|
+
cwd,
|
|
182
|
+
env: { ...process.env, ELM_HOME: elmHome },
|
|
183
|
+
stdout: "inherit",
|
|
184
|
+
stderr: "inherit"
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const exitCode = await buildChild.exited;
|
|
189
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
190
|
+
|
|
191
|
+
const compiledSource = await readFile(rawOutputPath, "utf8");
|
|
192
|
+
const esmSource = compiledSource.replace(/\}\(this\)\);?\s*$/, "}(globalThis));\n")
|
|
193
|
+
+ "\nconst Elm = globalThis.Elm;\nexport { Elm };\nexport default Elm;\n";
|
|
194
|
+
|
|
195
|
+
await writeFile(finalOutputPath, esmSource, "utf8");
|
|
196
|
+
await writeFile(sourceModulePath, `const source = ${JSON.stringify(esmSource)};\nexport default source;\n`, "utf8");
|
|
197
|
+
const compressed = gzipSync(Buffer.from(esmSource, "utf8"));
|
|
198
|
+
await writeFile(`${finalOutputPath}.gz`, compressed);
|
|
199
|
+
await rm(rawOutputPath, { force: true });
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
await mkdir(elmHome, { recursive: true });
|
|
203
|
+
|
|
204
|
+
for (const appConfig of config.apps) {
|
|
205
|
+
const exampleRoot = resolve(rootPath, appConfig.root);
|
|
206
|
+
const namespace = appConfig.module;
|
|
207
|
+
const namespacePath = resolve(exampleRoot, "src", moduleToPath(namespace));
|
|
208
|
+
const routes = await collectRoutes(namespace, resolve(namespacePath, "Routes"));
|
|
209
|
+
const islands = await collectIslands(namespace, resolve(namespacePath, "Islands"));
|
|
210
|
+
|
|
211
|
+
if (routes.length === 0) {
|
|
212
|
+
console.error(`No route modules found under ${resolve(namespacePath, "Routes")}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const wrapperDir = resolve(exampleRoot, ".elm-ssr");
|
|
217
|
+
const outputDir = resolve(rootPath, "generated", appConfig.root);
|
|
218
|
+
|
|
219
|
+
await mkdir(wrapperDir, { recursive: true });
|
|
220
|
+
|
|
221
|
+
// Sync ElmSsr source files from the CLI package to the project
|
|
222
|
+
const elmSsrSource = resolve(cliRoot, "elm-src");
|
|
223
|
+
const targetSourceDir = resolve(wrapperDir, "src");
|
|
224
|
+
await mkdir(targetSourceDir, { recursive: true });
|
|
225
|
+
|
|
226
|
+
const sync = Bun.spawn(["cp", "-r", `${elmSsrSource}/.`, targetSourceDir], {
|
|
227
|
+
stdout: "inherit",
|
|
228
|
+
stderr: "inherit"
|
|
229
|
+
});
|
|
230
|
+
await sync.exited;
|
|
231
|
+
|
|
232
|
+
await mkdir(outputDir, { recursive: true });
|
|
233
|
+
await rm(resolve(wrapperDir, "Generated"), { recursive: true, force: true });
|
|
234
|
+
await rm(resolve(wrapperDir, "Islands.elm"), { force: true });
|
|
235
|
+
await rm(resolve(outputDir, "islands"), { recursive: true, force: true });
|
|
236
|
+
|
|
237
|
+
await writeFile(resolve(wrapperDir, "Main.elm"), generateMain(routes), "utf8");
|
|
238
|
+
|
|
239
|
+
await compileEntrypoint({
|
|
240
|
+
cwd: exampleRoot,
|
|
241
|
+
entrypoint: resolve(wrapperDir, "Main.elm"),
|
|
242
|
+
outputDir,
|
|
243
|
+
outputName: "app"
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (islands.length > 0) {
|
|
247
|
+
await compileEntrypoint({
|
|
248
|
+
cwd: exampleRoot,
|
|
249
|
+
entrypoint: islands.map((island) => island.file),
|
|
250
|
+
outputDir,
|
|
251
|
+
outputName: "islands"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
await writeFile(resolve(outputDir, "islands-manifest.ts"), generateIslandsManifestModule(islands), "utf8");
|
|
255
|
+
}
|
|
256
|
+
};
|
package/lib/migrate.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { SQL } from "bun";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
listMigrations,
|
|
6
|
+
revertMigrations,
|
|
7
|
+
runMigrations
|
|
8
|
+
} from "../src/migrations.ts";
|
|
9
|
+
|
|
10
|
+
const USAGE = `\
|
|
11
|
+
elm-ssr migrate <up|down|status> [options]
|
|
12
|
+
|
|
13
|
+
Subcommands
|
|
14
|
+
up Apply pending *.sql migrations (alphabetical order).
|
|
15
|
+
down Revert the most-recently-applied migration (uses <name>.down.sql).
|
|
16
|
+
status Print applied (with timestamps) and pending migrations.
|
|
17
|
+
|
|
18
|
+
Options
|
|
19
|
+
--dir <path> Migration directory (default: ./migrations)
|
|
20
|
+
--db <conn> postgres://… URL, sqlite://path, or a plain SQLite file path.
|
|
21
|
+
If omitted, reads DATABASE_URL from the environment.
|
|
22
|
+
--count <n> How many to revert with 'down' (default 1)
|
|
23
|
+
--table <name> Tracking table name (default __elm_ssr_migrations)
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const findFlag = (args, name) => {
|
|
27
|
+
const i = args.indexOf(name);
|
|
28
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const sqliteAdapter = (path) => {
|
|
32
|
+
const db = new Database(path);
|
|
33
|
+
return {
|
|
34
|
+
adapter: {
|
|
35
|
+
exec: async (sql) => {
|
|
36
|
+
db.exec(sql);
|
|
37
|
+
},
|
|
38
|
+
list: async (sql) => db.query(sql).all()
|
|
39
|
+
},
|
|
40
|
+
close: async () => {
|
|
41
|
+
db.close();
|
|
42
|
+
},
|
|
43
|
+
label: `sqlite:${path}`
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const postgresAdapter = (url) => {
|
|
48
|
+
const sql = new SQL(url);
|
|
49
|
+
return {
|
|
50
|
+
adapter: {
|
|
51
|
+
exec: async (text) => {
|
|
52
|
+
await sql.unsafe(text);
|
|
53
|
+
},
|
|
54
|
+
list: async (text) => {
|
|
55
|
+
const rows = await sql.unsafe(text);
|
|
56
|
+
return Array.isArray(rows) ? rows : [...(rows ?? [])];
|
|
57
|
+
},
|
|
58
|
+
runInTransaction: async (fn) => {
|
|
59
|
+
await sql.begin(fn);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
close: async () => {
|
|
63
|
+
await sql.close();
|
|
64
|
+
},
|
|
65
|
+
label: `postgres ${new URL(url).host}${new URL(url).pathname}`
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const buildAdapter = (dbArg) => {
|
|
70
|
+
const target = dbArg ?? process.env.DATABASE_URL;
|
|
71
|
+
if (!target) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Missing --db (or DATABASE_URL). Examples:\n" +
|
|
74
|
+
" --db ./app.db\n" +
|
|
75
|
+
" --db sqlite://./app.db\n" +
|
|
76
|
+
" --db postgres://user:pass@localhost:5432/db"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (target.startsWith("postgres://") || target.startsWith("postgresql://")) {
|
|
80
|
+
return postgresAdapter(target);
|
|
81
|
+
}
|
|
82
|
+
if (target.startsWith("sqlite://")) {
|
|
83
|
+
return sqliteAdapter(target.slice("sqlite://".length));
|
|
84
|
+
}
|
|
85
|
+
return sqliteAdapter(target);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const migrate = async (args) => {
|
|
89
|
+
const sub = args[0];
|
|
90
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
91
|
+
process.stdout.write(USAGE);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!["up", "down", "status"].includes(sub)) {
|
|
95
|
+
process.stderr.write(`Unknown 'migrate' subcommand: ${sub}\n\n${USAGE}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const dir = resolve(process.cwd(), findFlag(args, "--dir") ?? "migrations");
|
|
100
|
+
const dbArg = findFlag(args, "--db");
|
|
101
|
+
const tableName = findFlag(args, "--table");
|
|
102
|
+
const countArg = findFlag(args, "--count");
|
|
103
|
+
const count = countArg !== undefined ? Number.parseInt(countArg, 10) : undefined;
|
|
104
|
+
|
|
105
|
+
const { adapter, close, label } = buildAdapter(dbArg);
|
|
106
|
+
console.log(`elm-ssr migrate ${sub} dir=${dir} db=${label}`);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (sub === "up") {
|
|
110
|
+
const result = await runMigrations(adapter, { dir, tableName });
|
|
111
|
+
if (result.applied.length === 0) {
|
|
112
|
+
console.log(`Nothing to apply (${result.skipped.length} already applied).`);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(`Applied ${result.applied.length}:`);
|
|
115
|
+
for (const file of result.applied) {
|
|
116
|
+
console.log(` + ${file}`);
|
|
117
|
+
}
|
|
118
|
+
if (result.skipped.length > 0) {
|
|
119
|
+
console.log(`Skipped ${result.skipped.length} already-applied.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (sub === "down") {
|
|
123
|
+
const result = await revertMigrations(adapter, { dir, tableName, count });
|
|
124
|
+
if (result.reverted.length === 0) {
|
|
125
|
+
console.log("Nothing to revert.");
|
|
126
|
+
} else {
|
|
127
|
+
console.log(`Reverted ${result.reverted.length}:`);
|
|
128
|
+
for (const file of result.reverted) {
|
|
129
|
+
console.log(` - ${file}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
const status = await listMigrations(adapter, { dir, tableName });
|
|
134
|
+
console.log(`Applied (${status.applied.length}):`);
|
|
135
|
+
for (const entry of status.applied) {
|
|
136
|
+
console.log(` [x] ${entry.name} (${entry.appliedAt})`);
|
|
137
|
+
}
|
|
138
|
+
console.log(`Pending (${status.pending.length}):`);
|
|
139
|
+
for (const file of status.pending) {
|
|
140
|
+
console.log(` [ ] ${file}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
await close();
|
|
145
|
+
}
|
|
146
|
+
};
|