elm-pages 2.1.9 → 2.1.10

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.
@@ -1,18 +1,23 @@
1
- const parseUrl = require("url").parse;
2
-
3
1
  // this middleware is only active when (config.base !== '/')
4
2
 
5
3
  module.exports = function baseMiddleware(base) {
6
4
  // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
7
5
  return function viteBaseMiddleware(req, res, next) {
8
- const url = req.url;
9
- const parsed = parseUrl(url);
10
- const path = parsed.pathname || "/";
6
+ // `req.url` only contains the path, since that is what gets passed in the HTTP request.
7
+ // See https://nodejs.org/api/http.html#http_message_url
8
+ const path = req.url;
11
9
 
12
- if (path.startsWith(base)) {
13
- // rewrite url to remove base.. this ensures that other middleware does
10
+ if (base === "/") {
11
+ // The RegExp will check for the slash, so we remove it here.
12
+ base = "";
13
+ }
14
+ // We want to detect the base at the beginning, hence the `^`,
15
+ // but also allow calling the base without a trailing slash, hence the `$`.
16
+ const baseRegExp = new RegExp(`^${base}(/|$)`);
17
+ if (baseRegExp.test(path)) {
18
+ // rewrite url to remove base. this ensures that other middleware does
14
19
  // not need to consider base being prepended or not
15
- req.url = url.replace(base, "/");
20
+ req.url = path.replace(baseRegExp, "/");
16
21
  return next();
17
22
  }
18
23
 
@@ -26,9 +31,10 @@ module.exports = function baseMiddleware(base) {
26
31
  } else if (req.headers.accept && req.headers.accept.includes("text/html")) {
27
32
  // non-based page visit
28
33
  res.statusCode = 404;
34
+ const suggestionUrl = `${base}/${path.slice(1)}`;
29
35
  res.end(
30
36
  `The server is configured with a public base URL of ${base} - ` +
31
- `did you mean to visit ${base}${url.slice(1)} instead?`
37
+ `did you mean to visit ${suggestionUrl} instead?`
32
38
  );
33
39
  return;
34
40
  }
@@ -344,12 +344,61 @@ async function compileCliApp(options) {
344
344
  );
345
345
 
346
346
  const elmFileContent = await fsPromises.readFile(ELM_FILE_PATH, "utf-8");
347
+ // Source: https://github.com/elm-explorations/test/blob/d5eb84809de0f8bbf50303efd26889092c800609/src/Elm/Kernel/HtmlAsJson.js
348
+ const forceThunksSource = ` _HtmlAsJson_toJson(x)
349
+ }
350
+
351
+ var virtualDomKernelConstants =
352
+ {
353
+ nodeTypeTagger: 4,
354
+ nodeTypeThunk: 5,
355
+ kids: "e",
356
+ refs: "l",
357
+ thunk: "m",
358
+ node: "k",
359
+ value: "a"
360
+ }
361
+
362
+ function forceThunks(vNode) {
363
+ if (typeof vNode !== "undefined" && vNode.$ === "#2") {
364
+ // This is a tuple (the kids : List (String, Html) field of a Keyed node); recurse into the right side of the tuple
365
+ vNode.b = forceThunks(vNode.b);
366
+ }
367
+ if (typeof vNode !== 'undefined' && vNode.$ === virtualDomKernelConstants.nodeTypeThunk && !vNode[virtualDomKernelConstants.node]) {
368
+ // This is a lazy node; evaluate it
369
+ var args = vNode[virtualDomKernelConstants.thunk];
370
+ vNode[virtualDomKernelConstants.node] = vNode[virtualDomKernelConstants.thunk].apply(args);
371
+ // And then recurse into the evaluated node
372
+ vNode[virtualDomKernelConstants.node] = forceThunks(vNode[virtualDomKernelConstants.node]);
373
+ }
374
+ if (typeof vNode !== 'undefined' && vNode.$ === virtualDomKernelConstants.nodeTypeTagger) {
375
+ // This is an Html.map; recurse into the node it is wrapping
376
+ vNode[virtualDomKernelConstants.node] = forceThunks(vNode[virtualDomKernelConstants.node]);
377
+ }
378
+ if (typeof vNode !== 'undefined' && typeof vNode[virtualDomKernelConstants.kids] !== 'undefined') {
379
+ // This is something with children (either a node with kids : List Html, or keyed with kids : List (String, Html));
380
+ // recurse into the children
381
+ vNode[virtualDomKernelConstants.kids] = vNode[virtualDomKernelConstants.kids].map(forceThunks);
382
+ }
383
+ return vNode;
384
+ }
385
+
386
+ function _HtmlAsJson_toJson(html) {
387
+ `;
388
+
347
389
  await fsPromises.writeFile(
348
390
  ELM_FILE_PATH,
349
391
  elmFileContent
350
392
  .replace(
351
393
  /return \$elm\$json\$Json\$Encode\$string\(.REPLACE_ME_WITH_JSON_STRINGIFY.\)/g,
352
- "return " + (options.debug ? "_Json_wrap(x)" : "x")
394
+ "return " +
395
+ (options.debug
396
+ ? `${forceThunksSource}
397
+ return _Json_wrap(forceThunks(html));
398
+ `
399
+ : `${forceThunksSource}
400
+ return forceThunks(html);
401
+ `)
353
402
  )
354
403
  .replace(
355
404
  "return ports ? { ports: ports } : {};",
@@ -47,6 +47,7 @@ async function main() {
47
47
  "Preserve the HTTP and JS Port cache instead of deleting it on server start"
48
48
  )
49
49
  .option("--base <basePath>", "serve site under a base path", "/")
50
+ .option("--https", "uses a https server")
50
51
  .action(async (options) => {
51
52
  if (!options.keepCache) {
52
53
  clearHttpAndPortCache();
@@ -108,16 +109,19 @@ function clearHttpAndPortCache() {
108
109
  }
109
110
 
110
111
  /**
111
- * @param {string} pagePath
112
+ * @param {string} rawPagePath
112
113
  */
113
- function normalizeUrl(pagePath) {
114
- if (!pagePath.startsWith("/")) {
115
- pagePath = "/" + pagePath;
116
- }
117
- if (!pagePath.endsWith("/")) {
118
- pagePath = pagePath + "/";
119
- }
120
- return pagePath;
114
+ function normalizeUrl(rawPagePath) {
115
+ const segments = rawPagePath
116
+ .split("/")
117
+ // Filter out all empty segments.
118
+ .filter((segment) => segment.length != 0);
119
+
120
+ // Do not add a trailing slash.
121
+ // The core issue is that `/base` is a prefix of `/base/`, but
122
+ // `/base/` is not a prefix of `/base`, which can later lead to issues
123
+ // with detecting whether the path contains the base.
124
+ return `/${segments.join("/")}`;
121
125
  }
122
126
 
123
127
  main();
@@ -8,6 +8,7 @@ const {
8
8
  runElmReview,
9
9
  } = require("./compile-elm.js");
10
10
  const http = require("http");
11
+ const https = require("https");
11
12
  const codegen = require("./codegen.js");
12
13
  const kleur = require("kleur");
13
14
  const serveStatic = require("serve-static");
@@ -17,9 +18,10 @@ const { Worker, SHARE_ENV } = require("worker_threads");
17
18
  const os = require("os");
18
19
  const { ensureDirSync } = require("./file-helpers.js");
19
20
  const baseMiddleware = require("./basepath-middleware.js");
21
+ const devcert = require("devcert");
20
22
 
21
23
  /**
22
- * @param {{ port: string; base: string }} options
24
+ * @param {{ port: string; base: string; https: boolean; }} options
23
25
  */
24
26
  async function start(options) {
25
27
  let threadReadyQueue = [];
@@ -28,6 +30,7 @@ async function start(options) {
28
30
  const cpuCount = os.cpus().length;
29
31
 
30
32
  const port = options.port;
33
+ const useHttps = options.https;
31
34
  let elmMakeRunning = true;
32
35
 
33
36
  const serve = serveStatic("public/", { index: false });
@@ -70,7 +73,7 @@ async function start(options) {
70
73
  });
71
74
  console.log(
72
75
  `${kleur.dim(`elm-pages dev server running at`)} ${kleur.green(
73
- `<http://localhost:${port}>`
76
+ `<${useHttps ? "https" : "http"}://localhost:${port}>`
74
77
  )}`
75
78
  );
76
79
  const poolSize = Math.max(1, cpuCount / 2 - 1);
@@ -118,7 +121,12 @@ async function start(options) {
118
121
  .use(serveStaticCode)
119
122
  .use(serve)
120
123
  .use(processRequest);
121
- http.createServer(app).listen(port);
124
+ if (useHttps) {
125
+ const ssl = await devcert.certificateFor("localhost");
126
+ https.createServer(ssl, app).listen(port);
127
+ } else {
128
+ http.createServer(app).listen(port);
129
+ }
122
130
  /**
123
131
  * @param {http.IncomingMessage} request
124
132
  * @param {http.ServerResponse} response
@@ -133,7 +141,6 @@ async function start(options) {
133
141
  }
134
142
 
135
143
  watcher.on("all", async function (eventName, pathThatChanged) {
136
- // console.log({ pathThatChanged });
137
144
  if (pathThatChanged === "elm.json") {
138
145
  watchElmSourceDirs(false);
139
146
  } else if (pathThatChanged.endsWith(".css")) {
@@ -168,12 +168,17 @@ view page maybePageUrl globalData pageData =
168
168
 
169
169
  _ ->
170
170
  { title = "Model mismatch", body = Html.text <| "Model mismatch" }
171
- , head = Page.${moduleName(name)}.page.head
171
+ , head = ${
172
+ phase === "browser"
173
+ ? "[]"
174
+ : `Page.${moduleName(name)}.page.head
172
175
  { data = data
173
176
  , sharedData = globalData
174
177
  , routeParams = ${emptyRouteParams(name) ? "{}" : "s"}
175
178
  , path = page.path
176
179
  }
180
+ `
181
+ }
177
182
  }
178
183
  `
179
184
  )
@@ -415,8 +420,12 @@ main =
415
420
  { init = init Nothing
416
421
  , urlToRoute = Route.urlToRoute
417
422
  , routeToPath = \\route -> route |> Maybe.map Route.routeToPath |> Maybe.withDefault []
418
- , site = Site.config
419
- , getStaticRoutes = getStaticRoutes |> DataSource.map (List.map Just)
423
+ , site = ${phase === "browser" ? `Nothing` : `Just Site.config`}
424
+ , getStaticRoutes = ${
425
+ phase === "browser"
426
+ ? `DataSource.succeed []`
427
+ : `getStaticRoutes |> DataSource.map (List.map Just)`
428
+ }
420
429
  , handleRoute = handleRoute
421
430
  , view = view
422
431
  , update = update
@@ -431,7 +440,11 @@ main =
431
440
  , fromJsPort = fromJsPort identity
432
441
  , data = dataForRoute
433
442
  , sharedData = Shared.template.data
434
- , apiRoutes = \\htmlToString -> pathsToGenerateHandler :: routePatterns :: manifestHandler :: Api.routes getStaticRoutes htmlToString
443
+ , apiRoutes = ${
444
+ phase === "browser"
445
+ ? `\\_ -> []`
446
+ : `\\htmlToString -> pathsToGenerateHandler :: routePatterns :: manifestHandler :: Api.routes getStaticRoutes htmlToString`
447
+ }
435
448
  , pathPatterns = routePatterns3
436
449
  , basePath = [ ${basePath
437
450
  .split("/")
@@ -19,6 +19,10 @@ async function run(name) {
19
19
  path.resolve(appRoot, "gitignore"),
20
20
  path.resolve(appRoot, ".gitignore")
21
21
  );
22
+ /* Since .elm-pages is in source-directories, make sure we create it before running any elm-pages commands
23
+ in case we run any install commands first. See: https://github.com/dillonkearns/elm-pages/issues/205
24
+ */
25
+ fs.mkdirSync(path.join(appRoot, ".elm-pages"), { recursive: true });
22
26
  } catch (err) {
23
27
  console.log(err);
24
28
  process.exit(1);
@@ -1,7 +1,7 @@
1
1
  const cliVersion = require("../../package.json").version;
2
2
  const seo = require("./seo-renderer.js");
3
3
  const elmPagesJsMinified = require("./elm-pages-js-minified.js");
4
- const path = require("path");
4
+ const path = require("path").posix;
5
5
  const jsesc = require("jsesc");
6
6
 
7
7
  /** @typedef { { head: any[]; errors: any[]; contentJson: any[]; html: string; route: string; title: string; } } Arg */
@@ -29,9 +29,9 @@ module.exports =
29
29
  <link rel="modulepreload" href="${path.join(basePath, "index.js")}">
30
30
  ${devServerOnly(
31
31
  /* html */ `<script defer="defer" src="${path.join(
32
- basePath,
33
- "hmr.js"
34
- )}" type="text/javascript"></script>`
32
+ basePath,
33
+ "hmr.js"
34
+ )}" type="text/javascript"></script>`
35
35
  )}
36
36
  <script defer="defer" src="${path.join(
37
37
  basePath,
@@ -45,7 +45,7 @@ ${elmPagesJsMinified}
45
45
  </script>
46
46
  <title>${fromElm.title}</title>
47
47
  <meta name="generator" content="elm-pages v${cliVersion}">
48
- <link rel="manifest" href="/manifest.json">
48
+ <link rel="manifest" href="${path.join(basePath, "manifest.json")}">
49
49
  <meta name="mobile-web-app-capable" content="yes">
50
50
  <meta name="theme-color" content="#ffffff">
51
51
  <meta name="apple-mobile-web-app-capable" content="yes">
@@ -81,10 +81,10 @@ function pathToRoot(cleanedRoute) {
81
81
  return cleanedRoute === ""
82
82
  ? cleanedRoute
83
83
  : cleanedRoute
84
- .split("/")
85
- .map((_) => "..")
86
- .join("/")
87
- .replace(/\.$/, "./");
84
+ .split("/")
85
+ .map((_) => "..")
86
+ .join("/")
87
+ .replace(/\.$/, "./");
88
88
  }
89
89
 
90
90
  function devServerStyleTag() {
@@ -60,7 +60,7 @@ function toString(/** @type { SeoTag[] } */ tags) {
60
60
  /** @typedef {{ name: string; attributes: string[][]; type: 'head' }} HeadTag */
61
61
  function appendTag(/** @type {HeadTag} */ tagDetails) {
62
62
  const tagsString = tagDetails.attributes.map(([name, value]) => {
63
- return `${name}="${value}"`;
63
+ return pairToAttribute([name, value]);
64
64
  });
65
65
  return ` <${tagDetails.name} ${tagsString.join(" ")} />`;
66
66
  }
@@ -77,5 +77,24 @@ ${JSON.stringify(tagDetails.contents)}
77
77
  * @returns string
78
78
  */
79
79
  function pairToAttribute([name, value]) {
80
- return `${name}="${value}"`;
80
+ return `${name}="${quoteattr(value)}"`;
81
+ }
82
+
83
+ function quoteattr(s, preserveCR) {
84
+ preserveCR = preserveCR ? "&#13;" : "\n";
85
+ return (
86
+ ("" + s) /* Forces the conversion to string. */
87
+ .replace(/&/g, "&amp;") /* This MUST be the 1st replacement. */
88
+ .replace(/'/g, "&apos;") /* The 4 other predefined entities, required. */
89
+ .replace(/"/g, "&quot;")
90
+ .replace(/</g, "&lt;")
91
+ .replace(/>/g, "&gt;")
92
+ /*
93
+ You may add other replacements here for HTML only
94
+ (but it's not necessary).
95
+ Or for XML, only if the named entities are defined in its DTD.
96
+ */
97
+ .replace(/\r\n/g, preserveCR) /* Must be before the next replacement. */
98
+ .replace(/[\r\n]/g, preserveCR)
99
+ );
81
100
  }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "elm-pages",
3
- "version": "2.1.9",
3
+ "version": "2.1.10",
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.",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "start": "cd examples/end-to-end && npm start",
10
- "test": "npx elmi-to-json --version && elm-verify-examples --run-tests && elm-test && (cd generator && mocha) && (cd examples/routing && npm i && npm run build -- --debug && elm-test)",
10
+ "test": "npx elmi-to-json --version && elm-verify-examples --run-tests && elm-test && (cd generator && mocha) && (cd examples/routing && npm i && npm run build -- --debug && elm-test) && npm run test:snapshot",
11
+ "test:snapshot": "(cd examples/escaping && npm install && npm test && cd ../..) && (cd examples/base-path && npm install && npm test && cd ../..)",
11
12
  "cypress": "npm start & cypress run --spec cypress/integration/elm-pages-dev.spec.js",
12
13
  "review": "elm-review"
13
14
  },
@@ -26,6 +27,7 @@
26
27
  "commander": "^8.1.0",
27
28
  "connect": "^3.7.0",
28
29
  "cross-spawn": "7.0.3",
30
+ "devcert": "^1.2.0",
29
31
  "elm-doc-preview": "^5.0.5",
30
32
  "elm-hot": "^1.1.6",
31
33
  "fs-extra": "^10.0.0",
@@ -25,8 +25,8 @@ mainView title =
25
25
  --, Attr.attribute "ref" reference
26
26
  , Attr.style "position" "absolute"
27
27
  , Attr.style "top" "0"
28
- , Attr.style "width" "1"
29
- , Attr.style "height" "1"
28
+ , Attr.style "width" "1px"
29
+ , Attr.style "height" "1px"
30
30
  , Attr.style "padding" "0"
31
31
  , Attr.style "overflow" "hidden"
32
32
  , Attr.style "clip" "rect(0, 0, 0, 0)"
@@ -155,8 +155,9 @@ just the metadata.
155
155
 
156
156
  blogPost : DataSource BlogPostMetadata
157
157
  blogPost =
158
- File.onlyFrontmatter "blog/hello-world.md"
158
+ File.onlyFrontmatter
159
159
  blogPostDecoder
160
+ "blog/hello-world.md"
160
161
 
161
162
  type alias BlogPostMetadata =
162
163
  { title : String
@@ -4,7 +4,7 @@ module DataSource.Glob exposing
4
4
  , captureFilePath
5
5
  , wildcard, recursiveWildcard
6
6
  , int, digits
7
- , expectUniqueMatch
7
+ , expectUniqueMatch, expectUniqueMatchFromList
8
8
  , literal
9
9
  , map, succeed, toDataSource
10
10
  , oneOf
@@ -195,7 +195,7 @@ That will give us
195
195
 
196
196
  ## Matching a Specific Number of Files
197
197
 
198
- @docs expectUniqueMatch
198
+ @docs expectUniqueMatch, expectUniqueMatchFromList
199
199
 
200
200
 
201
201
  ## Glob Patterns
@@ -1016,8 +1016,35 @@ expectUniqueMatch glob =
1016
1016
  DataSource.succeed file
1017
1017
 
1018
1018
  [] ->
1019
- DataSource.fail "No files matched."
1019
+ DataSource.fail <| "No files matched the pattern: " ++ toPatternString glob
1020
1020
 
1021
1021
  _ ->
1022
1022
  DataSource.fail "More than one file matched."
1023
1023
  )
1024
+
1025
+
1026
+ {-| -}
1027
+ expectUniqueMatchFromList : List (Glob a) -> DataSource a
1028
+ expectUniqueMatchFromList globs =
1029
+ globs
1030
+ |> List.map toDataSource
1031
+ |> DataSource.combine
1032
+ |> DataSource.andThen
1033
+ (\matchingFiles ->
1034
+ case List.concat matchingFiles of
1035
+ [ file ] ->
1036
+ DataSource.succeed file
1037
+
1038
+ [] ->
1039
+ DataSource.fail <| "No files matched the patterns: " ++ (globs |> List.map toPatternString |> String.join ", ")
1040
+
1041
+ _ ->
1042
+ DataSource.fail "More than one file matched."
1043
+ )
1044
+
1045
+
1046
+ toPatternString : Glob a -> String
1047
+ toPatternString glob =
1048
+ case glob of
1049
+ Glob pattern_ _ _ ->
1050
+ pattern_
@@ -555,20 +555,20 @@ lookupUrls requestInfo =
555
555
  []
556
556
 
557
557
 
558
- {-| Build off of the response from a previous `StaticHttp` request to build a follow-up request. You can use the data
558
+ {-| Build off of the response from a previous `DataSource` request to build a follow-up request. You can use the data
559
559
  from the previous response to build up the URL, headers, etc. that you send to the subsequent request.
560
560
 
561
561
  import DataSource
562
562
  import Json.Decode as Decode exposing (Decoder)
563
563
 
564
- licenseData : StaticHttp.Request String
564
+ licenseData : DataSource String
565
565
  licenseData =
566
- StaticHttp.get
566
+ DataSource.Http.get
567
567
  (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages")
568
568
  (Decode.at [ "license", "url" ] Decode.string)
569
- |> StaticHttp.andThen
569
+ |> DataSource.andThen
570
570
  (\licenseUrl ->
571
- StaticHttp.get (Secrets.succeed licenseUrl) (Decode.field "description" Decode.string)
571
+ DataSource.Http.get (Secrets.succeed licenseUrl) (Decode.field "description" Decode.string)
572
572
  )
573
573
 
574
574
  -}
package/src/Head.elm CHANGED
@@ -1,5 +1,5 @@
1
1
  module Head exposing
2
- ( Tag, metaName, metaProperty
2
+ ( Tag, metaName, metaProperty, metaRedirect
3
3
  , rssLink, sitemapLink, rootLanguage
4
4
  , structuredData
5
5
  , AttributeValue
@@ -17,7 +17,7 @@ so you can use the `Tag` type in your type annotations.
17
17
  But this module might be useful if you have a special use case, or if you are
18
18
  writing a plugin package to extend `elm-pages`.
19
19
 
20
- @docs Tag, metaName, metaProperty
20
+ @docs Tag, metaName, metaProperty, metaRedirect
21
21
  @docs rssLink, sitemapLink, rootLanguage
22
22
 
23
23
 
@@ -383,6 +383,21 @@ metaName name content =
383
383
  ]
384
384
 
385
385
 
386
+ {-| Example:
387
+
388
+ metaRedirect (Raw "0; url=https://google.com")
389
+
390
+ Results in `<meta http-equiv="refresh" content="0; url=https://google.com" />`
391
+
392
+ -}
393
+ metaRedirect : AttributeValue -> Tag
394
+ metaRedirect content =
395
+ node "meta"
396
+ [ ( "http-equiv", Raw "refresh" )
397
+ , ( "content", content )
398
+ ]
399
+
400
+
386
401
  {-| Low-level function for creating a tag for the HTML document's `<head>`.
387
402
  -}
388
403
  node : String -> List ( String, AttributeValue ) -> Tag
@@ -1,17 +1,18 @@
1
1
  module HtmlPrinter exposing (htmlToString)
2
2
 
3
- import ElmHtml.InternalTypes exposing (decodeElmHtml)
4
- import ElmHtml.ToString exposing (defaultFormatOptions, nodeToStringWithOptions)
5
3
  import Html exposing (Html)
6
4
  import Json.Decode as Decode
7
5
  import Json.Encode
6
+ import Test.Html.Internal.ElmHtml.InternalTypes exposing (decodeElmHtml)
7
+ import Test.Html.Internal.ElmHtml.ToString exposing (defaultFormatOptions, nodeToStringWithOptions)
8
+ import VirtualDom
8
9
 
9
10
 
10
11
  htmlToString : Html msg -> String
11
12
  htmlToString viewHtml =
12
13
  case
13
14
  Decode.decodeValue
14
- (decodeElmHtml (\_ _ -> Decode.succeed ()))
15
+ (decodeElmHtml (\_ _ -> VirtualDom.Normal (Decode.succeed ())))
15
16
  (asJsonView viewHtml)
16
17
  of
17
18
  Ok str ->