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.
@@ -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
+ };
@@ -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
+ };