Use in-memory fs or real fs for http cache depending on whether context is serverless.

This commit is contained in:
Dillon Kearns 2021-12-29 17:18:37 -08:00
parent 9a60119484
commit d13a397507
8 changed files with 196 additions and 73 deletions

View File

@ -139,7 +139,8 @@ async function render(event, context) {
mode,
event.path,
await reqToJson(event, requestTime),
addWatcher
addWatcher,
false
);
console.log("@@@renderResult", JSON.stringify(renderResult, null, 2));

View File

@ -8,3 +8,9 @@
targetPort = 1234
autoLaunch = true
framework = "#custom"
[[redirects]]
from = "/secret"
to = ".netlify/functions/secret"
status = 200
force = true

View File

@ -0,0 +1,3 @@
This is a top secret note. You can only access this if the elm-pages server lets you in. It's not a very good secret because this repo is open source, but if it were a private repo then you wouldn't be able to access this data without logging in first!
The secret is that there isn't actually a secret.

View File

@ -0,0 +1,132 @@
module Page.Secret exposing (Data, Model, Msg, page)
import DataSource exposing (DataSource)
import DataSource.File
import Head
import Head.Seo as Seo
import Html
import Html.Attributes as Attr
import Page exposing (Page, PageWithState, StaticPayload)
import PageServerResponse exposing (PageServerResponse)
import Pages.PageUrl exposing (PageUrl)
import Pages.Url
import Route
import Server.Request as Request
import ServerResponse
import Shared
import View exposing (View)
type alias Model =
{}
type alias Msg =
Never
type alias RouteParams =
{}
page : Page RouteParams Data
page =
Page.serverRender
{ head = head
, data = data
}
|> Page.buildNoState { view = view }
type Data
= LoggedIn LoggedInInfo
| NotLoggedIn
type alias LoggedInInfo =
{ username : String
, secretNote : String
}
data : RouteParams -> Request.Handler (PageServerResponse Data)
data routeParams =
Request.oneOfHandler
[ Request.expectCookie "username"
|> Request.thenRespond
(\username ->
username
|> LoggedInInfo
|> DataSource.succeed
|> DataSource.andMap (DataSource.File.rawFile "./secret-note.txt")
|> DataSource.map LoggedIn
|> DataSource.map PageServerResponse.RenderPage
)
, Request.succeed ()
|> Request.thenRespond
(\() ->
NotLoggedIn
|> DataSource.succeed
|> DataSource.map PageServerResponse.RenderPage
--"/login"
-- |> ServerResponse.temporaryRedirect
-- --|> ServerResponse.withStatusCode 404
-- |> PageServerResponse.ServerResponse
-- |> DataSource.succeed
)
]
head :
StaticPayload Data RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = Pages.Url.external "TODO"
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
view :
Maybe PageUrl
-> Shared.Model
-> StaticPayload Data RouteParams
-> View Msg
view maybeUrl sharedModel static =
case static.data of
LoggedIn loggedInInfo ->
{ title = "Secret"
, body =
[ Html.main_ [ Attr.style "max-width" "800px" ]
[ Html.h1 [] [ Html.text "This is a secret page" ]
, Html.p []
[ Html.text <| "Welcome, " ++ loggedInInfo.username ++ "!"
]
, Html.p []
[ Html.text loggedInInfo.secretNote
]
]
]
}
NotLoggedIn ->
{ title = "Secret"
, body =
[ Html.main_ [ Attr.style "max-width" "800px" ]
[ Html.h1 [] [ Html.text "You're not logged in" ]
, Route.link Route.Login
[]
[ Html.text <| "Login" ]
]
]
}

View File

@ -20,7 +20,8 @@ async function run({ mode, pathname, serverRequest }) {
if (mode === "dev-server" && patterns.size > 0) {
parentPort.postMessage({ tag: "watch", data: [...patterns] });
}
}
},
true
);
if (mode === "dev-server") {

View File

@ -3,12 +3,10 @@
const path = require("path");
const matter = require("gray-matter");
const globby = require("globby");
// const fsPromises = require("fs").promises;
const fsPromises = require("memfs").promises;
const fsPromises = require("fs").promises;
const preRenderHtml = require("./pre-render-html.js");
const { lookupOrPerform } = require("./request-cache.js");
const kleur = require("kleur");
const { fs, Volume, vol } = require("memfs");
kleur.enabled = true;
process.on("unhandledRejection", (error) => {
@ -26,6 +24,7 @@ module.exports =
* @param {string} path
* @param {{ method: string; hostname: string; query: Record<string, string | undefined>; headers: Record<string, string>; host: string; pathname: string; port: number | null; protocol: string; rawUrl: string; }} request
* @param {(pattern: string) => void} addDataSourceWatcher
* @param {boolean} hasFsAccess
* @returns
*/
async function run(
@ -34,9 +33,14 @@ module.exports =
mode,
path,
request,
addDataSourceWatcher
addDataSourceWatcher,
hasFsAccess
) {
vol.reset();
console.log({ hasFsAccess });
const { fs, resetInMemoryFs } = require("./request-cache-fs.js")(
hasFsAccess
);
resetInMemoryFs();
foundErrors = false;
pendingDataSourceResponses = [];
pendingDataSourceCount = 0;
@ -49,7 +53,9 @@ module.exports =
mode,
path,
request,
addDataSourceWatcher
addDataSourceWatcher,
fs,
hasFsAccess
);
return result;
};
@ -69,7 +75,9 @@ function runElmApp(
mode,
pagePath,
request,
addDataSourceWatcher
addDataSourceWatcher,
fs,
hasFsAccess
) {
const isDevServer = mode !== "build";
let patternsToWatch = new Set();
@ -150,7 +158,7 @@ function runElmApp(
}
} else if (fromElm.tag === "DoHttp") {
const requestToPerform = fromElm.args[0];
runHttpJob(app, mode, requestToPerform);
runHttpJob(app, mode, requestToPerform, fs, hasFsAccess);
} else if (fromElm.tag === "Glob") {
const globPattern = fromElm.args[0];
patternsToWatch.add(globPattern);
@ -218,9 +226,7 @@ function jsonOrNull(string) {
async function runJob(app, filePath) {
pendingDataSourceCount += 1;
try {
const fileContents =
// await fsPromises.readFile(path.join(process.cwd(), filePath))
(await fsPromises.readFile(filePath)).toString();
const fileContents = (await fsPromises.readFile(filePath)).toString();
const parsedFile = matter(fileContents);
pendingDataSourceResponses.push({
@ -246,6 +252,8 @@ async function runJob(app, filePath) {
}),
});
} catch (e) {
console.log(fsPromises);
console.error("222@@@", e);
sendError(app, {
title: "Error reading file",
message: `A DataSource.File read failed because I couldn't find this file: ${kleur.yellow(
@ -258,18 +266,19 @@ async function runJob(app, filePath) {
}
}
async function runHttpJob(app, mode, requestToPerform) {
async function runHttpJob(app, mode, requestToPerform, fs, hasFsAccess) {
pendingDataSourceCount += 1;
try {
const responseFilePath = await lookupOrPerform(
mode,
requestToPerform.unmasked
requestToPerform.unmasked,
hasFsAccess
);
pendingDataSourceResponses.push({
request: requestToPerform,
response: (
await fsPromises.readFile(responseFilePath, "utf8")
await fs.promises.readFile(responseFilePath, "utf8")
).toString(),
});
} catch (error) {
@ -319,51 +328,6 @@ function flushQueue(app) {
});
}
/**
* @param {string} filePath
* @returns {Promise<Object>}
*/
async function readFileTask(app, filePath) {
// console.log(`Read file ${filePath}`);
try {
const fileContents = (
await fsPromises.readFile(path.join(process.cwd(), filePath))
).toString();
// console.log(`DONE reading file ${filePath}`);
const parsedFile = matter(fileContents);
return {
request: {
masked: {
url: `file://${filePath}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
unmasked: {
url: `file://${filePath}`,
method: "GET",
headers: [],
body: { tag: "EmptyBody", args: [] },
},
},
response: JSON.stringify({
parsedFrontmatter: parsedFile.data,
withoutFrontmatter: parsedFile.content,
rawFile: fileContents,
jsonFile: jsonOrNull(fileContents),
}),
};
} catch (e) {
sendError(app, {
title: "Error reading file",
message: `A DataSource.File read failed because I couldn't find this file: ${kleur.yellow(
filePath
)}`,
});
}
}
/**
* @param {string} globPattern
* @returns {Promise<Object>}

View File

@ -0,0 +1,13 @@
module.exports = function (/** @type {boolean} */ hasFsAccess) {
if (hasFsAccess) {
return {
fs: require("fs"),
resetInMemoryFs: () => {},
};
} else {
return {
fs: require("memfs").fs,
resetInMemoryFs: require("memfs").vol.reset,
};
}
};

View File

@ -1,9 +1,7 @@
const path = require("path");
const undici = require("undici");
// const fs = require("fs");
const objectHash = require("object-hash");
const kleur = require("kleur");
const fs = require("memfs");
/**
* To cache HTTP requests on disk with quick lookup and insertion, we store the hashed request.
@ -18,14 +16,17 @@ function requestToString(request) {
/**
* @param {Object} request
*/
function fullPath(request) {
// return path.join(
// process.cwd(),
// ".elm-pages",
// "http-response-cache",
// requestToString(request)
// );
return path.join("/", requestToString(request));
function fullPath(request, hasFsAccess) {
if (hasFsAccess) {
return path.join(
process.cwd(),
".elm-pages",
"http-response-cache",
requestToString(request)
);
} else {
return path.join("/", requestToString(request));
}
}
/**
@ -33,10 +34,12 @@ function fullPath(request) {
* @param {{url: string; headers: {[x: string]: string}; method: string; body: Body } } rawRequest
* @returns {Promise<string>}
*/
function lookupOrPerform(mode, rawRequest) {
function lookupOrPerform(mode, rawRequest, hasFsAccess) {
console.log({ hasFsAccess });
const { fs } = require("./request-cache-fs.js")(hasFsAccess);
return new Promise(async (resolve, reject) => {
const request = toRequest(rawRequest);
const responsePath = fullPath(request);
const responsePath = fullPath(request, hasFsAccess);
if (fs.existsSync(responsePath)) {
// console.log("Skipping request, found file.");