elm-pages 3.0.0-beta.5 → 3.0.0-beta.7

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.
@@ -75,7 +75,7 @@ console.elmlog = (str) => logs.push(str + "\n");
75
75
  const { Elm } = require("./Runner.elm.js");
76
76
 
77
77
  // Start the Elm app
78
- const flags = { initialSeed: 4067606959, fuzzRuns: 100, filter: null };
78
+ const flags = { initialSeed: 2254786925, fuzzRuns: 100, filter: null };
79
79
  const app = Elm.Runner.init({ flags: flags });
80
80
 
81
81
  // Record the timing at which we received the last "runTest" message
@@ -82,7 +82,7 @@ const verbosity = 0;
82
82
  // Create a long lived reporter worker
83
83
  const { Elm } = require("./Reporter.elm.js");
84
84
  const flags = {
85
- initialSeed: 4067606959,
85
+ initialSeed: 2254786925,
86
86
  fuzzRuns: 100,
87
87
  mode: "consoleNoColor",
88
88
  verbosity: verbosity,
@@ -75,7 +75,7 @@ console.elmlog = (str) => logs.push(str + "\n");
75
75
  const { Elm } = require("./Runner.elm.js");
76
76
 
77
77
  // Start the Elm app
78
- const flags = { initialSeed: 1129837679, fuzzRuns: 100, filter: null };
78
+ const flags = { initialSeed: 3791732356, fuzzRuns: 100, filter: null };
79
79
  const app = Elm.Runner.init({ flags: flags });
80
80
 
81
81
  // Record the timing at which we received the last "runTest" message
@@ -82,7 +82,7 @@ const verbosity = 0;
82
82
  // Create a long lived reporter worker
83
83
  const { Elm } = require("./Reporter.elm.js");
84
84
  const flags = {
85
- initialSeed: 1129837679,
85
+ initialSeed: 3791732356,
86
86
  fuzzRuns: 100,
87
87
  mode: "consoleNoColor",
88
88
  verbosity: verbosity,
@@ -16,6 +16,8 @@ const { build } = require("vite");
16
16
  const preRenderHtml = require("./pre-render-html.js");
17
17
  const esbuild = require("esbuild");
18
18
  const { createHash } = require("crypto");
19
+ const { merge_vite_configs } = require("./vite-utils.js");
20
+ const { resolveConfig } = require("./config.js");
19
21
 
20
22
  let pool = [];
21
23
  let pagesReady;
@@ -75,40 +77,31 @@ async function run(options) {
75
77
  const generateCode = codegen.generate(options.base);
76
78
 
77
79
  await generateCode;
80
+
81
+ const config = await resolveConfig();
78
82
  await fsPromises.writeFile(
79
83
  "elm-stuff/elm-pages/index.html",
80
- preRenderHtml.templateHtml()
84
+ preRenderHtml.templateHtml(false, config.headTagsTemplate)
81
85
  );
82
-
83
- const config = await import(
84
- path.join(process.cwd(), "elm-pages.config.mjs")
85
- )
86
- .then(async (elmPagesConfig) => {
87
- return elmPagesConfig.default || {};
88
- })
89
- .catch((error) => {
90
- console.warn(
91
- "No `elm-pages.config.mjs` file found. Using default config."
92
- );
93
- return {};
94
- });
95
- const viteConfig = config.vite || {};
96
-
97
- const buildComplete = build({
98
- configFile: false,
99
- root: process.cwd(),
100
- base: options.base,
101
- ssr: false,
102
-
103
- build: {
104
- manifest: true,
105
- outDir: "dist",
106
- rollupOptions: {
107
- input: "elm-stuff/elm-pages/index.html",
86
+ const viteConfig = merge_vite_configs(
87
+ {
88
+ configFile: false,
89
+ root: process.cwd(),
90
+ base: options.base,
91
+ ssr: false,
92
+
93
+ build: {
94
+ manifest: true,
95
+ outDir: "dist",
96
+ rollupOptions: {
97
+ input: "elm-stuff/elm-pages/index.html",
98
+ },
108
99
  },
109
100
  },
110
- ...viteConfig,
111
- });
101
+ config.vite || {}
102
+ );
103
+
104
+ const buildComplete = build(viteConfig);
112
105
  const compileClientDone = compileElm(options);
113
106
  await buildComplete;
114
107
  await compileClientDone;
@@ -0,0 +1,39 @@
1
+ const path = require("path");
2
+
3
+ async function resolveConfig() {
4
+ return await import(path.join(process.cwd(), "elm-pages.config.mjs"))
5
+ .then(async (elmPagesConfig) => {
6
+ return (
7
+ elmPagesConfig.default || {
8
+ headTagsTemplate: defaultHeadTagsTemplate,
9
+ }
10
+ );
11
+ })
12
+ .catch((error) => {
13
+ console.warn(
14
+ "No `elm-pages.config.mjs` file found. Using default config."
15
+ );
16
+ return {
17
+ headTagsTemplate: defaultHeadTagsTemplate,
18
+ vite: {},
19
+ };
20
+ });
21
+ }
22
+
23
+ function defaultHeadTagsTemplate(context) {
24
+ return `
25
+ <link rel="stylesheet" href="/style.css" />
26
+ <meta charset="UTF-8" />
27
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
28
+ <meta name="generator" content="elm-pages v${context.cliVersion}" />
29
+ <meta name="mobile-web-app-capable" content="yes" />
30
+ <meta name="theme-color" content="#ffffff" />
31
+ <meta name="apple-mobile-web-app-capable" content="yes" />
32
+ <meta
33
+ name="apple-mobile-web-app-status-bar-style"
34
+ content="black-translucent"
35
+ />
36
+ `;
37
+ }
38
+
39
+ module.exports = { resolveConfig };
@@ -24,6 +24,9 @@ const busboy = require("busboy");
24
24
  const { createServer: createViteServer } = require("vite");
25
25
  const cliVersion = require("../../package.json").version;
26
26
  const esbuild = require("esbuild");
27
+ const { merge_vite_configs } = require("./vite-utils.js");
28
+ const { templateHtml } = require("./pre-render-html.js");
29
+ const { resolveConfig } = require("./config.js");
27
30
 
28
31
  /**
29
32
  * @param {{ port: string; base: string; https: boolean; debug: boolean; }} options
@@ -117,27 +120,23 @@ async function start(options) {
117
120
  watcher.add(sourceDirs);
118
121
  }
119
122
 
120
- const viteConfig = await import(
121
- path.join(process.cwd(), "elm-pages.config.mjs")
122
- )
123
- .then(async (elmPagesConfig) => {
124
- return elmPagesConfig.default.vite || {};
125
- })
126
- .catch((error) => {
127
- console.warn(
128
- kleur.yellow(
129
- "No `elm-pages.config.mjs` file found. Using default config."
130
- )
131
- );
132
- return {};
133
- });
134
- const vite = await createViteServer({
135
- server: { middlewareMode: "ssr", base: options.base, port: options.port },
136
- configFile: false,
137
- root: process.cwd(),
138
- base: options.base,
139
- ...viteConfig,
140
- });
123
+ const config = await resolveConfig();
124
+ const vite = await createViteServer(
125
+ merge_vite_configs(
126
+ {
127
+ server: {
128
+ middlewareMode: "ssr",
129
+ base: options.base,
130
+ port: options.port,
131
+ },
132
+ configFile: false,
133
+ root: process.cwd(),
134
+ base: options.base,
135
+ },
136
+
137
+ config.vite
138
+ )
139
+ );
141
140
  esbuild
142
141
  .build({
143
142
  entryPoints: ["./port-data-source"],
@@ -459,35 +458,7 @@ async function start(options) {
459
458
  }
460
459
  case "html": {
461
460
  try {
462
- const template =
463
- /*html*/
464
- `<!DOCTYPE html>
465
- <!-- ROOT --><html lang="en">
466
- <head>
467
- <script src="/hmr.js" type="text/javascript"></script>
468
- <script src="/elm.js" type="text/javascript"></script>
469
- <link rel="stylesheet" href="/style.css">
470
- <link rel="stylesheet" href="/dev-style.css">
471
- <script src="/elm-pages.js" type="module"></script>
472
- <meta charset="UTF-8" />
473
- <meta name="viewport" content="width=device-width,initial-scale=1" />
474
- <title><!-- PLACEHOLDER_TITLE --></title>
475
- <meta name="generator" content="elm-pages v${cliVersion}" />
476
- <meta name="mobile-web-app-capable" content="yes" />
477
- <meta name="theme-color" content="#ffffff" />
478
- <meta name="apple-mobile-web-app-capable" content="yes" />
479
- <meta
480
- name="apple-mobile-web-app-status-bar-style"
481
- content="black-translucent"
482
- />
483
- <!-- PLACEHOLDER_HEAD_AND_DATA -->
484
- </head>
485
- <body>
486
- <div data-url="" display="none"></div>
487
- <!-- PLACEHOLDER_HTML -->
488
- </body>
489
- </html>
490
- `;
461
+ const template = templateHtml(true, config.headTagsTemplate);
491
462
  const processedTemplate = await vite.transformIndexHtml(
492
463
  req.originalUrl,
493
464
  template
@@ -17,28 +17,34 @@ function wrapHtml(basePath, fromElm, contentDatPayload) {
17
17
  };
18
18
  }
19
19
 
20
- function templateHtml() {
20
+ /**
21
+ * @param {boolean} devMode
22
+ * @param {(context: {cliVersion: string;}) => string} userHeadTagsTemplate
23
+ */
24
+ function templateHtml(devMode, userHeadTagsTemplate) {
21
25
  return /* html */ `<!DOCTYPE html>
22
26
  <!-- ROOT --><html lang="en">
23
27
  <head>
24
- <!-- PLACEHOLDER_PRELOADS -->
25
- <script defer src="/elm.js" type="text/javascript"></script>
26
- <script defer src="${path.join(
27
- __dirname,
28
- "../static-code/elm-pages.js"
29
- )}" type="module"></script>
30
- <link rel="stylesheet" href="/style.css" />
31
- <meta charset="UTF-8" />
32
- <meta name="viewport" content="width=device-width,initial-scale=1" />
33
28
  <title><!-- PLACEHOLDER_TITLE --></title>
34
- <meta name="generator" content="elm-pages v${cliVersion}" />
35
- <meta name="mobile-web-app-capable" content="yes" />
36
- <meta name="theme-color" content="#ffffff" />
37
- <meta name="apple-mobile-web-app-capable" content="yes" />
38
- <meta
39
- name="apple-mobile-web-app-status-bar-style"
40
- content="black-translucent"
41
- />
29
+ ${
30
+ devMode
31
+ ? `
32
+ <script src="/hmr.js" type="text/javascript"></script>
33
+ <link rel="stylesheet" href="/dev-style.css">`
34
+ : `
35
+ <!-- PLACEHOLDER_PRELOADS -->`
36
+ }
37
+ <script defer src="/elm.js" type="text/javascript"></script>
38
+ ${
39
+ devMode
40
+ ? `<script src="/elm-pages.js" type="module"></script>
41
+ `
42
+ : `<script defer src="${path.join(
43
+ __dirname,
44
+ "../static-code/elm-pages.js"
45
+ )}" type="module"></script>`
46
+ }
47
+ ${indent(userHeadTagsTemplate({ cliVersion }))}
42
48
  <!-- PLACEHOLDER_HEAD_AND_DATA -->
43
49
  </head>
44
50
  <body>
@@ -48,6 +54,16 @@ function templateHtml() {
48
54
  </html>`;
49
55
  }
50
56
 
57
+ /**
58
+ * @param {string} snippet
59
+ */
60
+ function indent(snippet) {
61
+ return snippet
62
+ .split("\n")
63
+ .map((line) => ` ${line}`)
64
+ .join("\n");
65
+ }
66
+
51
67
  /**
52
68
  * @param {string} processedTemplate
53
69
  */
@@ -0,0 +1,78 @@
1
+ /** This code is from https://github.com/sveltejs/kit/blob/3b457f67d4d7c59fc63bb3f600a490e4dacc2e62/packages/kit/src/exports/vite/utils.js */
2
+
3
+ /**
4
+ * @param {...import('vite').UserConfig} configs
5
+ * @returns {import('vite').UserConfig}
6
+ */
7
+ function merge_vite_configs(...configs) {
8
+ return deep_merge(
9
+ ...configs.map((config) => ({
10
+ ...config,
11
+ resolve: {
12
+ ...config.resolve,
13
+ alias: normalize_alias(config.resolve?.alias || {}),
14
+ },
15
+ }))
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Takes zero or more objects and returns a new object that has all the values
21
+ * deeply merged together. None of the original objects will be mutated at any
22
+ * level, and the returned object will have no references to the original
23
+ * objects at any depth. If there's a conflict the last one wins, except for
24
+ * arrays which will be combined.
25
+ * @param {...Object} objects
26
+ * @returns {Record<string, any>} the merged object
27
+ */
28
+ function deep_merge(...objects) {
29
+ const result = {};
30
+ /** @type {string[]} */
31
+ objects.forEach((o) => merge_into(result, o));
32
+ return result;
33
+ }
34
+
35
+ /**
36
+ * normalize kit.vite.resolve.alias as an array
37
+ * @param {import('vite').AliasOptions} o
38
+ * @returns {import('vite').Alias[]}
39
+ */
40
+ function normalize_alias(o) {
41
+ if (Array.isArray(o)) return o;
42
+ return Object.entries(o).map(([find, replacement]) => ({
43
+ find,
44
+ replacement,
45
+ }));
46
+ }
47
+
48
+ /**
49
+ * Merges b into a, recursively, mutating a.
50
+ * @param {Record<string, any>} a
51
+ * @param {Record<string, any>} b
52
+ */
53
+ function merge_into(a, b) {
54
+ /**
55
+ * Checks for "plain old Javascript object", typically made as an object
56
+ * literal. Excludes Arrays and built-in types like Buffer.
57
+ * @param {any} x
58
+ */
59
+ const is_plain_object = (x) =>
60
+ typeof x === "object" && x.constructor === Object;
61
+
62
+ for (const prop in b) {
63
+ if (is_plain_object(b[prop])) {
64
+ if (!is_plain_object(a[prop])) {
65
+ a[prop] = {};
66
+ }
67
+ merge_into(a[prop], b[prop]);
68
+ } else if (Array.isArray(b[prop])) {
69
+ if (!Array.isArray(a[prop])) {
70
+ a[prop] = [];
71
+ }
72
+ a[prop].push(...b[prop]);
73
+ } else {
74
+ a[prop] = b[prop];
75
+ }
76
+ }
77
+ }
78
+ module.exports = { merge_vite_configs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elm-pages",
3
- "version": "3.0.0-beta.5",
3
+ "version": "3.0.0-beta.7",
4
4
  "homepage": "https://elm-pages.com",
5
5
  "moduleResolution": "node",
6
6
  "description": "Type-safe static sites, written in pure elm with your own custom elm-markup syntax.",
@@ -45,7 +45,7 @@
45
45
  "object-hash": "^2.2.0",
46
46
  "serve-static": "^1.15.0",
47
47
  "terser": "^5.14.2",
48
- "vite": "^3.0.9",
48
+ "vite": "^3.1.8",
49
49
  "which": "^2.0.2"
50
50
  },
51
51
  "devDependencies": {
@@ -123,16 +123,16 @@ A common use for this is to map your data into your elm-pages view:
123
123
  map : (a -> b) -> DataSource a -> DataSource b
124
124
  map fn requestInfo =
125
125
  case requestInfo of
126
- RequestError error ->
127
- RequestError error
126
+ ApiRoute value ->
127
+ ApiRoute (fn value)
128
128
 
129
129
  Request urls lookupFn ->
130
130
  Request
131
131
  urls
132
132
  (mapLookupFn fn lookupFn)
133
133
 
134
- ApiRoute value ->
135
- ApiRoute (fn value)
134
+ RequestError error ->
135
+ RequestError error
136
136
 
137
137
 
138
138
  mapLookupFn : (a -> b) -> (d -> c -> DataSource a) -> d -> c -> DataSource b
@@ -182,8 +182,8 @@ resolve =
182
182
 
183
183
  -}
184
184
  combine : List (DataSource value) -> DataSource (List value)
185
- combine =
186
- List.foldr (map2 (::)) (succeed [])
185
+ combine items =
186
+ List.foldl (map2 (::)) (succeed []) items |> map List.reverse
187
187
 
188
188
 
189
189
  {-| Like map, but it takes in two `Request`s.
@@ -212,11 +212,8 @@ combine =
212
212
  map2 : (a -> b -> c) -> DataSource a -> DataSource b -> DataSource c
213
213
  map2 fn request1 request2 =
214
214
  case ( request1, request2 ) of
215
- ( RequestError error, _ ) ->
216
- RequestError error
217
-
218
- ( _, RequestError error ) ->
219
- RequestError error
215
+ ( ApiRoute value1, ApiRoute value2 ) ->
216
+ ApiRoute (fn value1 value2)
220
217
 
221
218
  ( Request urls1 lookupFn1, Request urls2 lookupFn2 ) ->
222
219
  Request
@@ -233,8 +230,11 @@ map2 fn request1 request2 =
233
230
  urls1
234
231
  (mapReq fn (\_ _ -> ApiRoute value2) lookupFn1)
235
232
 
236
- ( ApiRoute value1, ApiRoute value2 ) ->
237
- ApiRoute (fn value1 value2)
233
+ ( RequestError error, _ ) ->
234
+ RequestError error
235
+
236
+ ( _, RequestError error ) ->
237
+ RequestError error
238
238
 
239
239
 
240
240
  mapReq : (a -> b -> c) -> (e -> d -> DataSource a) -> (e -> d -> DataSource b) -> e -> d -> DataSource c
@@ -247,9 +247,6 @@ mapReq fn lookupFn1 lookupFn2 maybeMock rawResponses =
247
247
  lookup : Maybe Pages.StaticHttpRequest.MockResolver -> DataSource value -> RequestsAndPending -> Result Pages.StaticHttpRequest.Error value
248
248
  lookup maybeMockResolver requestInfo rawResponses =
249
249
  case requestInfo of
250
- RequestError error ->
251
- Err error
252
-
253
250
  Request urls lookupFn ->
254
251
  lookup maybeMockResolver
255
252
  (addUrls urls (lookupFn maybeMockResolver rawResponses))
@@ -258,18 +255,21 @@ lookup maybeMockResolver requestInfo rawResponses =
258
255
  ApiRoute value ->
259
256
  Ok value
260
257
 
258
+ RequestError error ->
259
+ Err error
260
+
261
261
 
262
262
  addUrls : List HashRequest.Request -> DataSource value -> DataSource value
263
263
  addUrls urlsToAdd requestInfo =
264
264
  case requestInfo of
265
- RequestError error ->
266
- RequestError error
265
+ ApiRoute value ->
266
+ ApiRoute value
267
267
 
268
268
  Request initialUrls function ->
269
269
  Request (initialUrls ++ urlsToAdd) function
270
270
 
271
- ApiRoute value ->
272
- ApiRoute value
271
+ RequestError error ->
272
+ RequestError error
273
273
 
274
274
 
275
275
  {-| The full details to perform a StaticHttp request.
@@ -286,14 +286,14 @@ type alias RequestDetails =
286
286
  lookupUrls : DataSource value -> List RequestDetails
287
287
  lookupUrls requestInfo =
288
288
  case requestInfo of
289
- RequestError _ ->
290
- -- TODO should this have URLs passed through?
289
+ ApiRoute _ ->
291
290
  []
292
291
 
293
292
  Request urls _ ->
294
293
  urls
295
294
 
296
- ApiRoute _ ->
295
+ RequestError _ ->
296
+ -- TODO should this have URLs passed through?
297
297
  []
298
298
 
299
299
 
@@ -324,19 +324,19 @@ andThen fn requestInfo =
324
324
  rawResponses
325
325
  |> (\result ->
326
326
  case result of
327
- Err error ->
328
- RequestError error
329
-
330
327
  Ok value ->
331
328
  case fn value of
329
+ ApiRoute finalValue ->
330
+ ApiRoute finalValue
331
+
332
332
  Request values function ->
333
333
  Request values function
334
334
 
335
335
  RequestError error ->
336
336
  RequestError error
337
337
 
338
- ApiRoute finalValue ->
339
- ApiRoute finalValue
338
+ Err error ->
339
+ RequestError error
340
340
  )
341
341
  )
342
342
 
@@ -1,8 +1,28 @@
1
- module Pages.Generate exposing (Type(..), serverRender, buildWithLocalState, buildNoState, Builder)
1
+ module Pages.Generate exposing
2
+ ( buildWithLocalState, buildNoState, Builder
3
+ , Type(..)
4
+ , serverRender
5
+ , preRender, single
6
+ )
2
7
 
3
8
  {-|
4
9
 
5
- @docs Type, serverRender, buildWithLocalState, buildNoState, Builder
10
+
11
+ ## Initializing the Generator Builder
12
+
13
+ @docs buildWithLocalState, buildNoState, Builder
14
+
15
+ @docs Type
16
+
17
+
18
+ ## Generating Server-Rendered Pages
19
+
20
+ @docs serverRender
21
+
22
+
23
+ ## Generating pre-rendered pages
24
+
25
+ @docs preRender, single
6
26
 
7
27
  -}
8
28
 
@@ -30,12 +50,18 @@ typeToDeclaration name type_ =
30
50
 
31
51
  {-| -}
32
52
  type Builder
33
- = Builder
53
+ = ServerRender
34
54
  { data : ( Type, Elm.Expression -> Elm.Expression )
35
55
  , action : ( Type, Elm.Expression -> Elm.Expression )
36
56
  , head : Elm.Expression -> Elm.Expression
37
57
  , moduleName : List String
38
58
  }
59
+ | PreRender
60
+ { data : ( Type, Elm.Expression -> Elm.Expression )
61
+ , pages : Maybe Elm.Expression
62
+ , head : Elm.Expression -> Elm.Expression
63
+ , moduleName : List String
64
+ }
39
65
 
40
66
 
41
67
  {-| -}
@@ -47,7 +73,56 @@ serverRender :
47
73
  }
48
74
  -> Builder
49
75
  serverRender =
50
- Builder
76
+ ServerRender
77
+
78
+
79
+ {-| -}
80
+ preRender :
81
+ { data : ( Type, Elm.Expression -> Elm.Expression )
82
+ , pages : Elm.Expression
83
+ , head : Elm.Expression -> Elm.Expression
84
+ , moduleName : List String
85
+ }
86
+ -> Builder
87
+ preRender input =
88
+ --let
89
+ -- hasDynamicRouteSegments : Bool
90
+ -- hasDynamicRouteSegments =
91
+ -- RoutePattern.fromModuleName input.moduleName
92
+ -- -- TODO give error if not parseable here
93
+ -- |> Maybe.map RoutePattern.hasRouteParams
94
+ -- |> Maybe.withDefault False
95
+ --in
96
+ PreRender
97
+ { data = input.data
98
+ , pages =
99
+ input.pages
100
+ |> Elm.withType
101
+ (Elm.Annotation.namedWith [ "DataSource" ]
102
+ "DataSource"
103
+ [ Elm.Annotation.list <| Elm.Annotation.named [] "RouteParams"
104
+ ]
105
+ )
106
+ |> Just
107
+ , head = input.head
108
+ , moduleName = input.moduleName
109
+ }
110
+
111
+
112
+ {-| -}
113
+ single :
114
+ { data : ( Type, Elm.Expression )
115
+ , head : Elm.Expression -> Elm.Expression
116
+ , moduleName : List String
117
+ }
118
+ -> Builder
119
+ single input =
120
+ PreRender
121
+ { data = ( Tuple.first input.data, \_ -> Tuple.second input.data )
122
+ , pages = Nothing
123
+ , head = input.head
124
+ , moduleName = input.moduleName
125
+ }
51
126
 
52
127
 
53
128
  {-| -}
@@ -56,20 +131,42 @@ buildNoState :
56
131
  }
57
132
  -> Builder
58
133
  -> Elm.File
59
- buildNoState definitions (Builder builder) =
60
- userFunction builder.moduleName
61
- { view = \_ -> definitions.view
62
- , localState = Nothing
63
- , data = builder.data |> Tuple.second
64
- , action = builder.action |> Tuple.second
65
- , head = builder.head
66
- , types =
67
- { model = Alias (Elm.Annotation.record [])
68
- , msg = Alias Elm.Annotation.unit
69
- , data = builder.data |> Tuple.first
70
- , actionData = builder.action |> Tuple.first
71
- }
72
- }
134
+ buildNoState definitions builder_ =
135
+ case builder_ of
136
+ ServerRender builder ->
137
+ userFunction builder.moduleName
138
+ { view = \_ -> definitions.view
139
+ , localState = Nothing
140
+ , data = builder.data |> Tuple.second
141
+ , action = builder.action |> Tuple.second |> Action
142
+ , head = builder.head
143
+ , types =
144
+ { model = Alias (Elm.Annotation.record [])
145
+ , msg = Alias Elm.Annotation.unit
146
+ , data = builder.data |> Tuple.first
147
+ , actionData = builder.action |> Tuple.first
148
+ }
149
+ }
150
+
151
+ PreRender builder ->
152
+ userFunction builder.moduleName
153
+ { view = \_ -> definitions.view
154
+ , localState = Nothing
155
+ , data = builder.data |> Tuple.second
156
+ , action = builder.pages |> Pages
157
+ , head = builder.head
158
+ , types =
159
+ { model = Alias (Elm.Annotation.record [])
160
+ , msg = Alias Elm.Annotation.unit
161
+ , data = builder.data |> Tuple.first
162
+ , actionData =
163
+ Elm.Annotation.namedWith [ "DataSource" ]
164
+ "DataSource"
165
+ [ Elm.Annotation.list (Elm.Annotation.named [] "RouteParams")
166
+ ]
167
+ |> Alias
168
+ }
169
+ }
73
170
 
74
171
 
75
172
  {-| -}
@@ -83,25 +180,57 @@ buildWithLocalState :
83
180
  }
84
181
  -> Builder
85
182
  -> Elm.File
86
- buildWithLocalState definitions (Builder builder) =
87
- userFunction builder.moduleName
88
- { view = definitions.view
89
- , localState =
90
- Just
91
- { update = definitions.update
92
- , init = definitions.init
93
- , subscriptions = definitions.subscriptions
183
+ buildWithLocalState definitions builder_ =
184
+ case builder_ of
185
+ ServerRender builder ->
186
+ userFunction builder.moduleName
187
+ { view = definitions.view
188
+ , localState =
189
+ Just
190
+ { update = definitions.update
191
+ , init = definitions.init
192
+ , subscriptions = definitions.subscriptions
193
+ }
194
+ , data = builder.data |> Tuple.second
195
+ , action = builder.action |> Tuple.second |> Action
196
+ , head = builder.head
197
+ , types =
198
+ { model = definitions.model
199
+ , msg = definitions.msg
200
+ , data = builder.data |> Tuple.first
201
+ , actionData = builder.action |> Tuple.first
202
+ }
203
+ }
204
+
205
+ PreRender builder ->
206
+ userFunction builder.moduleName
207
+ { view = definitions.view
208
+ , localState =
209
+ Just
210
+ { update = definitions.update
211
+ , init = definitions.init
212
+ , subscriptions = definitions.subscriptions
213
+ }
214
+ , data = builder.data |> Tuple.second
215
+ , action = builder.pages |> Pages
216
+ , head = builder.head
217
+ , types =
218
+ { model = definitions.model
219
+ , msg = definitions.msg
220
+ , data = builder.data |> Tuple.first
221
+ , actionData =
222
+ Elm.Annotation.namedWith [ "DataSource" ]
223
+ "DataSource"
224
+ [ Elm.Annotation.list (Elm.Annotation.named [] "RouteParams")
225
+ ]
226
+ |> Alias
227
+ }
94
228
  }
95
- , data = builder.data |> Tuple.second
96
- , action = builder.action |> Tuple.second
97
- , head = builder.head
98
- , types =
99
- { model = definitions.model
100
- , msg = definitions.msg
101
- , data = builder.data |> Tuple.first
102
- , actionData = builder.action |> Tuple.first
103
- }
104
- }
229
+
230
+
231
+ type ActionOrPages
232
+ = Action (Elm.Expression -> Elm.Expression)
233
+ | Pages (Maybe Elm.Expression)
105
234
 
106
235
 
107
236
  {-| -}
@@ -116,7 +245,7 @@ userFunction :
116
245
  , subscriptions : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression
117
246
  }
118
247
  , data : Elm.Expression -> Elm.Expression
119
- , action : Elm.Expression -> Elm.Expression
248
+ , action : ActionOrPages
120
249
  , head : Elm.Expression -> Elm.Expression
121
250
  , types : { model : Type, msg : Type, data : Type, actionData : Type }
122
251
  }
@@ -225,25 +354,84 @@ userFunction moduleName definitions =
225
354
  }
226
355
  )
227
356
 
228
- dataFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression }
357
+ dataFn : { declaration : Elm.Declaration, call : List Elm.Expression -> Elm.Expression, callFrom : List String -> List Elm.Expression -> Elm.Expression }
229
358
  dataFn =
230
- Elm.Declare.fn "data"
231
- ( "routeParams"
232
- , "RouteParams"
233
- |> Elm.Annotation.named []
234
- |> Just
235
- )
236
- (definitions.data >> Elm.withType (myType "Data"))
359
+ case definitions.action of
360
+ Pages Nothing ->
361
+ Elm.Declare.function "data"
362
+ []
363
+ (\_ ->
364
+ definitions.data Elm.unit
365
+ |> Elm.withType
366
+ (case definitions.action of
367
+ Pages _ ->
368
+ Elm.Annotation.namedWith [ "DataSource" ]
369
+ "DataSource"
370
+ [ Elm.Annotation.named [] "Data"
371
+ ]
372
+
373
+ Action _ ->
374
+ myType "Data"
375
+ )
376
+ )
237
377
 
238
- actionFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression }
378
+ _ ->
379
+ Elm.Declare.function "data"
380
+ [ ( "routeParams"
381
+ , "RouteParams"
382
+ |> Elm.Annotation.named []
383
+ |> Just
384
+ )
385
+ ]
386
+ (\args ->
387
+ case args of
388
+ [ arg ] ->
389
+ definitions.data arg
390
+ |> Elm.withType
391
+ (case definitions.action of
392
+ Pages _ ->
393
+ Elm.Annotation.namedWith [ "DataSource" ]
394
+ "DataSource"
395
+ [ Elm.Annotation.named [] "Data"
396
+ ]
397
+
398
+ Action _ ->
399
+ myType "Data"
400
+ )
401
+
402
+ _ ->
403
+ Elm.unit
404
+ )
405
+
406
+ actionFn : Maybe { declaration : Elm.Declaration, call : List Elm.Expression -> Elm.Expression, callFrom : List String -> List Elm.Expression -> Elm.Expression }
239
407
  actionFn =
240
- Elm.Declare.fn "action"
241
- ( "routeParams"
242
- , "RouteParams"
243
- |> Elm.Annotation.named []
244
- |> Just
245
- )
246
- (definitions.action >> Elm.withType (myType "ActionData"))
408
+ case definitions.action of
409
+ Action action_ ->
410
+ Elm.Declare.function "action"
411
+ [ ( "routeParams"
412
+ , "RouteParams"
413
+ |> Elm.Annotation.named []
414
+ |> Just
415
+ )
416
+ ]
417
+ (\args ->
418
+ case args of
419
+ [ arg ] ->
420
+ action_ arg |> Elm.withType (myType "ActionData")
421
+
422
+ _ ->
423
+ Elm.unit
424
+ )
425
+ |> Just
426
+
427
+ Pages pages_ ->
428
+ pages_
429
+ |> Maybe.map
430
+ (\justPagesExpression ->
431
+ Elm.Declare.function "pages"
432
+ []
433
+ (\_ -> justPagesExpression)
434
+ )
247
435
 
248
436
  headFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression }
249
437
  headFn =
@@ -268,49 +456,87 @@ userFunction moduleName definitions =
268
456
  )
269
457
  )
270
458
  , Elm.declaration "route"
271
- (serverRender_
272
- { action =
273
- \routeParams ->
274
- actionFn.call routeParams
275
- |> Elm.withType (myType "ActionData")
276
- , data =
277
- \routeParams ->
278
- dataFn.call routeParams
279
- |> Elm.withType (myType "Data")
280
- , head = headFn.call
281
- }
282
- |> (case localDefinitions of
283
- Just local ->
284
- buildWithLocalState_
285
- { view = viewFn.call
286
- , update = local.updateFn.call
287
- , init = local.initFn.call
288
- , subscriptions = local.subscriptionsFn.call
289
- }
290
- >> Elm.withType
291
- (Elm.Annotation.namedWith [ "RouteBuilder" ]
292
- "StatefulRoute"
293
- [ localType "RouteParams"
294
- , localType "Data"
295
- , localType "ActionData"
296
- , localType "Model"
297
- , localType "Msg"
298
- ]
299
- )
459
+ (case definitions.action of
460
+ Action _ ->
461
+ serverRender_
462
+ { action =
463
+ \routeParams ->
464
+ actionFn
465
+ |> Maybe.map
466
+ (\justActionFn ->
467
+ justActionFn.call [ routeParams ]
468
+ |> Elm.withType (myType "ActionData")
469
+ )
470
+ |> Maybe.withDefault Elm.unit
471
+ , data =
472
+ \routeParams ->
473
+ dataFn.call [ routeParams ]
474
+ |> Elm.withType (myType "Data")
475
+ , head = headFn.call
476
+ }
300
477
 
478
+ Pages _ ->
479
+ (case actionFn of
301
480
  Nothing ->
302
- buildNoState_
303
- { view = viewFn.call Elm.unit
481
+ single_
482
+ { data =
483
+ dataFn.call []
484
+ |> Elm.withType
485
+ (Elm.Annotation.namedWith [ "DataSource" ]
486
+ "DataSource"
487
+ [ Elm.Annotation.named [] "Data"
488
+ ]
489
+ )
490
+ , head = headFn.call
304
491
  }
305
- >> Elm.withType
306
- (Elm.Annotation.namedWith [ "RouteBuilder" ]
307
- "StatelessRoute"
308
- [ localType "RouteParams"
309
- , localType "Data"
310
- , localType "ActionData"
311
- ]
312
- )
313
- )
492
+
493
+ Just justActionFn ->
494
+ preRender_
495
+ { pages = justActionFn.call []
496
+ , data =
497
+ \routeParams ->
498
+ dataFn.call [ routeParams ]
499
+ |> Elm.withType
500
+ (Elm.Annotation.namedWith [ "DataSource" ]
501
+ "DataSource"
502
+ [ Elm.Annotation.named [] "Data"
503
+ ]
504
+ )
505
+ , head = headFn.call
506
+ }
507
+ )
508
+ |> (case localDefinitions of
509
+ Just local ->
510
+ buildWithLocalState_
511
+ { view = viewFn.call
512
+ , update = local.updateFn.call
513
+ , init = local.initFn.call
514
+ , subscriptions = local.subscriptionsFn.call
515
+ }
516
+ >> Elm.withType
517
+ (Elm.Annotation.namedWith [ "RouteBuilder" ]
518
+ "StatefulRoute"
519
+ [ localType "RouteParams"
520
+ , localType "Data"
521
+ , localType "ActionData"
522
+ , localType "Model"
523
+ , localType "Msg"
524
+ ]
525
+ )
526
+
527
+ Nothing ->
528
+ buildNoState_
529
+ { view = viewFn.call Elm.unit
530
+ }
531
+ >> Elm.withType
532
+ (Elm.Annotation.namedWith [ "RouteBuilder" ]
533
+ "StatelessRoute"
534
+ [ localType "RouteParams"
535
+ , localType "Data"
536
+ , localType "ActionData"
537
+ ]
538
+ )
539
+ )
314
540
  )
315
541
  ]
316
542
  ++ (case localDefinitions of
@@ -326,10 +552,13 @@ userFunction moduleName definitions =
326
552
  ++ [ definitions.types.data |> typeToDeclaration "Data"
327
553
  , definitions.types.actionData |> typeToDeclaration "ActionData"
328
554
  , dataFn.declaration
329
- , actionFn.declaration
330
555
  , headFn.declaration
331
556
  , viewFn.declaration
332
557
  ]
558
+ ++ ([ actionFn |> Maybe.map .declaration
559
+ ]
560
+ |> List.filterMap identity
561
+ )
333
562
  )
334
563
 
335
564
 
@@ -463,6 +692,128 @@ serverRender_ serverRenderArg =
463
692
  ]
464
693
 
465
694
 
695
+ preRender_ :
696
+ { data : Elm.Expression -> Elm.Expression
697
+ , pages : Elm.Expression
698
+ , head : Elm.Expression -> Elm.Expression
699
+ }
700
+ -> Elm.Expression
701
+ preRender_ serverRenderArg =
702
+ Elm.apply
703
+ (Elm.value
704
+ { importFrom = [ "RouteBuilder" ]
705
+ , name = "preRender"
706
+ , annotation =
707
+ Just
708
+ (Elm.Annotation.function
709
+ [ Elm.Annotation.record
710
+ [ ( "data"
711
+ , Elm.Annotation.function
712
+ [ Elm.Annotation.var "routeParams" ]
713
+ (Elm.Annotation.namedWith
714
+ [ "DataSource" ]
715
+ "DataSource"
716
+ [ Elm.Annotation.var "data"
717
+ ]
718
+ )
719
+ )
720
+ , ( "pages"
721
+ , Elm.Annotation.namedWith
722
+ [ "DataSource" ]
723
+ "DataSource"
724
+ [ Elm.Annotation.list (Elm.Annotation.named [] "RouteParams")
725
+ ]
726
+ )
727
+ , ( "head"
728
+ , Elm.Annotation.function
729
+ [ Elm.Annotation.namedWith
730
+ [ "RouteBuilder" ]
731
+ "StaticPayload"
732
+ [ Elm.Annotation.var "data"
733
+ , Elm.Annotation.var "action"
734
+ , Elm.Annotation.var "routeParams"
735
+ ]
736
+ ]
737
+ (Elm.Annotation.list
738
+ (Elm.Annotation.namedWith [ "Head" ] "Tag" [])
739
+ )
740
+ )
741
+ ]
742
+ ]
743
+ (Elm.Annotation.namedWith
744
+ [ "RouteBuilder" ]
745
+ "Builder"
746
+ [ Elm.Annotation.named [] "RouteParams"
747
+ , Elm.Annotation.named [] "Data"
748
+ , Elm.Annotation.named [] "Action"
749
+ ]
750
+ )
751
+ )
752
+ }
753
+ )
754
+ [ Elm.record
755
+ [ Tuple.pair "data" (Elm.functionReduced "serverRenderUnpack" serverRenderArg.data)
756
+ , Tuple.pair "pages" serverRenderArg.pages
757
+ , Tuple.pair "head" (Elm.functionReduced "serverRenderUnpack" serverRenderArg.head)
758
+ ]
759
+ ]
760
+
761
+
762
+ single_ :
763
+ { data : Elm.Expression
764
+ , head : Elm.Expression -> Elm.Expression
765
+ }
766
+ -> Elm.Expression
767
+ single_ serverRenderArg =
768
+ Elm.apply
769
+ (Elm.value
770
+ { importFrom = [ "RouteBuilder" ]
771
+ , name = "single"
772
+ , annotation =
773
+ Just
774
+ (Elm.Annotation.function
775
+ [ Elm.Annotation.record
776
+ [ ( "data"
777
+ , Elm.Annotation.namedWith
778
+ [ "DataSource" ]
779
+ "DataSource"
780
+ [ Elm.Annotation.var "data"
781
+ ]
782
+ )
783
+ , ( "head"
784
+ , Elm.Annotation.function
785
+ [ Elm.Annotation.namedWith
786
+ [ "RouteBuilder" ]
787
+ "StaticPayload"
788
+ [ Elm.Annotation.var "data"
789
+ , Elm.Annotation.var "action"
790
+ , Elm.Annotation.var "routeParams"
791
+ ]
792
+ ]
793
+ (Elm.Annotation.list
794
+ (Elm.Annotation.namedWith [ "Head" ] "Tag" [])
795
+ )
796
+ )
797
+ ]
798
+ ]
799
+ (Elm.Annotation.namedWith
800
+ [ "RouteBuilder" ]
801
+ "Builder"
802
+ [ Elm.Annotation.named [] "RouteParams"
803
+ , Elm.Annotation.named [] "Data"
804
+ , Elm.Annotation.named [] "Action"
805
+ ]
806
+ )
807
+ )
808
+ }
809
+ )
810
+ [ Elm.record
811
+ [ Tuple.pair "data" serverRenderArg.data
812
+ , Tuple.pair "head" (Elm.functionReduced "serverRenderUnpack" serverRenderArg.head)
813
+ ]
814
+ ]
815
+
816
+
466
817
  buildWithLocalState_ :
467
818
  { view :
468
819
  Elm.Expression
@@ -423,9 +423,7 @@ update config appMsg model =
423
423
  )
424
424
 
425
425
  else
426
- ( { model
427
- | url = url
428
- }
426
+ ( model
429
427
  , NoEffect
430
428
  )
431
429
  -- TODO is it reasonable to always re-fetch route data if you re-navigate to the current route? Might be a good
@@ -605,27 +603,32 @@ update config appMsg model =
605
603
  , actionData = newActionData
606
604
  }
607
605
 
608
- ( userModel, _ ) =
606
+ ( userModel, userEffect ) =
609
607
  -- TODO if urlWithoutRedirectResolution is different from the url with redirect resolution, then
610
608
  -- instead of calling update, call pushUrl (I think?)
611
609
  -- TODO include user Cmd
612
- config.update model.pageFormState
613
- (model.inFlightFetchers |> toFetcherState)
614
- (model.transition |> Maybe.map Tuple.second)
615
- newSharedData
616
- newPageData
617
- model.key
618
- (config.onPageChange
619
- { protocol = model.url.protocol
620
- , host = model.url.host
621
- , port_ = model.url.port_
622
- , path = urlPathToPath urlWithoutRedirectResolution
623
- , query = urlWithoutRedirectResolution.query
624
- , fragment = urlWithoutRedirectResolution.fragment
625
- , metadata = config.urlToRoute urlWithoutRedirectResolution
626
- }
627
- )
628
- previousPageData.userModel
610
+ if stayingOnSamePath then
611
+ ( previousPageData.userModel, NoEffect )
612
+
613
+ else
614
+ config.update model.pageFormState
615
+ (model.inFlightFetchers |> toFetcherState)
616
+ (model.transition |> Maybe.map Tuple.second)
617
+ newSharedData
618
+ newPageData
619
+ model.key
620
+ (config.onPageChange
621
+ { protocol = model.url.protocol
622
+ , host = model.url.host
623
+ , port_ = model.url.port_
624
+ , path = urlPathToPath urlWithoutRedirectResolution
625
+ , query = urlWithoutRedirectResolution.query
626
+ , fragment = urlWithoutRedirectResolution.fragment
627
+ , metadata = config.urlToRoute urlWithoutRedirectResolution
628
+ }
629
+ )
630
+ previousPageData.userModel
631
+ |> Tuple.mapSecond UserCmd
629
632
 
630
633
  updatedModel : Model userModel pageData actionData sharedData
631
634
  updatedModel =
@@ -656,10 +659,13 @@ update config appMsg model =
656
659
  , currentPath = newUrl.path
657
660
  }
658
661
  , if not stayingOnSamePath && scrollToTopWhenDone then
659
- ScrollToTop
662
+ Batch
663
+ [ ScrollToTop
664
+ , userEffect
665
+ ]
660
666
 
661
667
  else
662
- NoEffect
668
+ userEffect
663
669
  )
664
670
  |> (case maybeUserMsg of
665
671
  Just userMsg ->
@@ -1392,7 +1398,8 @@ loadDataAndUpdateUrl ( newPageData, newSharedData, newActionData ) maybeUserMsg
1392
1398
  , actionData = newActionData
1393
1399
  }
1394
1400
 
1395
- ( userModel, _ ) =
1401
+ -- TODO use userEffect here?
1402
+ ( userModel, userEffect ) =
1396
1403
  -- TODO if urlWithoutRedirectResolution is different from the url with redirect resolution, then
1397
1404
  -- instead of calling update, call pushUrl (I think?)
1398
1405
  -- TODO include user Cmd