Browser doesn't recognize html files written with node file system [duplicate] - javascript

This question already has answers here:
Why don't self-closing script elements work?
(12 answers)
Closed 4 years ago.
I'm building an application using React an Apollo 2 and I'm trying to write a script to generate static html files for server-side rendering.
I'm currently using this logic to render my React component on the server and it's working fine:
export default (Page) => (req, res) => {
const App = (
<ApolloProvider client={client(fetch)}>
<Main>
<Page />
</Main>
</ApolloProvider>
);
getDataFromTree(App).then(() => {
const data = {
html: ReactDOMServer.renderToString(App),
seo: { title: "Home", description: "" }
};
res.render("index", { data });
});
}
But I wanted to generate the .html files so I could serve them statically, without running this method every time. I tried to compile the result of renderToString to an .ejs file and then write to a file using fs, but it didn't work. This is the code:
const component = ReactDOMServer.renderToString(App);
const template = ejs.compile(fs.readFileSync(path.join(__dirname, 'landing_template.ejs'), 'utf-8'));
const html = template({ component });
fs.writeFile(
path.join(__dirname, "../views/landing.ejs"),
html,
err => {
if(err) console.log(err);
else console.log("done.");
});
The file was written successfully but If I open my Page Source the part added by the compile method is grayed out and seems to be ignored:
I also tried just reading the file and replacing the string using .replace and inserting a dummy div, but it's always the same result.
// using compile
const template = ejs.compile(fs.readFileSync(path.join(__dirname, 'landing_template.ejs'), 'utf-8'));
const html = template({ component });
const template = ejs.compile(fs.readFileSync(path.join(__dirname, 'landing_template.ejs'), 'utf-8'));
const html = template({ component: "<div>teste</div>" });
// using readFile
const template = fs.readFileSync(path.join(__dirname, 'landing_template.ejs'), 'utf-8');
const html = template.replace("<%- component %>", component);
const template = fs.readFileSync(path.join(__dirname, 'landing_template.ejs'), 'utf-8');
const html = template.replace("<%- component %>", "<div>teste</div>");
// maybe this is the problem?
fs.writeFile(
path.join(__dirname, "../views/landing.ejs"),
html,
{ encoding: "utf8" },
err => {
if(err) console.log(err);
else console.log("done.");
});
I guess the browser is not recognizing this new text as HTML and failing to parse it. Does any one know a way I can make this work?
Thanks!

<script /> is wrong. You have to use this:
<script src="URL"></script>

What a cool idea! This is probably happening because, as you mentioned, the browser is not recognizing the fact that you are sending html. Try explicitly setting the Content-Type header to text/html before calling render:
export default (Page) => (req, res) => {
const App = (
<ApolloProvider client={client(fetch)}>
<Main>
<Page />
</Main>
</ApolloProvider>
);
getDataFromTree(App).then(() => {
const data = {
html: ReactDOMServer.renderToString(App),
seo: { title: "Home", description: "" }
};
res.setHeader("Content-Type", "text/html");
res.render("index", { data });
});
}
Edit:
Ah I see it now! You cannot self-close <script> tags in HTML unfortunately, and this is tripping the browser up. Close the script tag in your header like so:
<script async src="/build/app.bundle.js" type="text/javascript"></script>
That should fix the problem.

Related

Using oak, serving HTML without extension? (Deno)

Using Oak, how can I serve HTML without the extension? e.g.
host:port/home.html -> host:port/home
Here's my current code to render my public/views folder:
router.get('/:page', async (ctx: Context, next: () => Promise<unknown>) => {
await send(ctx, ctx.request.url.pathname, {
root: join(Deno.cwd(), 'public', 'views'),
extensions: ['htm', 'html']
});
await next();
});
The extensions option is not working or maybe I just use it the wrong way.
Edit
My fix is currently removing the .html extension (e.g. home.html -> home). Pretty sure there's a better way than this
You can use this to send the file:
router.get('/path', async (ctx:any) => {
const text = await Deno.readTextFile('./file.html');
ctx.response.headers.set("Content-Type", "text/html")
ctx.response.body = text;
});

How do I properly consume and present cloud function server data via Ajax requests?

I'm trying to consume a json response from an Express style server in Firebase's Cloud functions, but I'm unable to present the response in the DOM. The response path and status (200) are good, but the response data I'm getting in the browser is my entire index.HTML page, not the json data.
Here's my basic set up in the cloud functions:
app.get("/tester", (req, res) => {
res.json({ serverData: "Hello" });
});
exports.app = functions.https.onRequest(app);
and my React FE code to consume it:
function App() {
let I;
const onClick = () => {
axios.get("/tester").then(res => {
I = res.data.serverData;
console.log(I);
});
};
return (
<div>
<button onClick={onClick}>click</button>
<div>{I}</div>
</div>
);
}
Like I said above, the response data I'm getting in the dev tools is just the barebones index.html page, not the text I want to receive. How can I map this data to the DOM?
You need I to be defined as a 'state' property within your function. Easiest way to do that is use React's useState hook. First add this to your script by including:
import React, { useState } from 'react';
Then declare I using the useState hook and, after retrieving the data, use the associated setter to set the new value:
function App() {
let [I, setI] = useState('');
const onClick = () => {
axios.get("/tester").then(res => {
setI(res.data.serverData);
console.log(I);
});
};
return (
<div>
<button onClick={onClick}>click</button>
<div>{I}</div>
</div>
);
}

Dynamic import of Javascript module from stream

Goal: To support dynamic loading of Javascript modules contingent on some security or defined user role requirement such that even if the name of the module is identified in dev tools, it cannot be successfully imported via the console.
A JavaScript module can be easily uploaded to a cloud storage service like Firebase (#AskFirebase) and the code can be conditionally retrieved using a Firebase Cloud Function firebase.functions().httpsCallable("ghost"); based on the presence of a custom claim or similar test.
export const ghost = functions.https.onCall(async (data, context) => {
if (! context.auth.token.restrictedAccess === true) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
}
const storage = new Storage();
const bucketName = 'bucket-name.appspot.com';
const srcFilename = 'RestrictedChunk.chunk.js';
// Downloads the file
const response = await storage
.bucket(bucketName)
.file(srcFilename).download();
const code = String.fromCharCode.apply(String, response[0])
return {source: code};
})
In the end, what I want to do...
...is take a webpack'ed React component, put it in the cloud, conditionally download it to the client after a server-side security check, and import() it into the user's client environment and render it.
Storing the Javascript in the cloud and conditionally downloading to the client are easy. Once I have the webpack'ed code in the client, I can use Function(downloadedRestrictedComponent) to add it to the user's environment much as one would use import('./RestrictedComponent') but what I can't figure out is how to get the default export from the component so I can actually render the thing.
import(pathToComponent) returns the loaded module, and as far as I know there is no option to pass import() a string or a stream, just a path to the module. And Function(downloadedComponent) will add the downloaded code into the client environment but I don't know how to access the module's export(s) to render the dynamically loaded React components.
Is there any way to dynamically import a Javascript module from a downloaded stream?
Edit to add: Thanks for the reply. Not familiar with the nuances of Blobs and URL.createObjectURL. Any idea why this would be not found?
const ghost = firebase.functions().httpsCallable("ghost");
const LoadableRestricted = Loadable({
// loader: () => import(/* webpackChunkName: "Restricted" */ "./Restricted"),
loader: async () => {
const ghostContents = await ghost();
console.log(ghostContents);
const myBlob = new Blob([ghostContents.data.source], {
type: "application/javascript"
});
console.log(myBlob);
const myURL = URL.createObjectURL(myBlob);
console.log(myURL);
return import(myURL);
},
render(loaded, props) {
console.log(loaded);
let Component = loaded.Restricted;
return <Component {...props} />;
},
loading: Loading,
delay: 2000
});
Read the contents of the module file/stream into a BLOB. The use URL.createObjectURL() to create your dynamic URL to the BLOB. Now use import as you suggested above:
import(myBlobURL).then(module=>{/*doSomethingWithModule*/});
You can try using React.lazy:
import React, {lazy, Suspense} from 'react';
const Example = () => {
const [userAuthenticated, setUserAuthenticated] = useState(true);
if (userAthenticated) {
const RestrictedComponent = lazy(() => import('./RestrictedComponent'));
return (
<div>
<Suspense fallback={<div><p>Loading...</p></div>}>
<RestrictedComponent />
</Suspense>
</div>
)
}
return (
<div>
<h1>404</h1>
<p>Restricted</p>
</div>
);
}
export default Example;

Calling javascript function on load pug - setting localStorage with param

What I am attempting to do is invoke a JS function from a pug template that will set an item on the localStorage. The value of this item will be a param that was passed to the pug template "token".
Similar questions have been asked in the past and I took inspiration from these answers as they seem to be similar:
Pug call js function from another file inside template
Express / Jade / Pug: Calling a javascript object's functions
My resulting code is:
users.js
import setToken from "../src/setToken";
router.post("/signin", (req, res) => {
...
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function(result) {
res.render("congrats", {
title: "Congrats",
token: result.getAccessToken().getJwtToken(),
setToken: setToken
});
},
onFailure: function(err) {
next(err);
}
});
});
setToken.js
export default (token) => {
console.log("::: setToken :::");
localStorage.setItem("cognitoToken", token);
};
congrats.pug
extends layout
block scripts
script
| var setToken = #{setToken};
| setToken(#{token})
block content
...
layout.pug includes a client-side JS script which also has a function storeToken. I have attempted to call that as well but nothing .
Nothing is output to the console and nothing is set in localStorage. I am unsure if this is the best way to even achieve this but it appears my JS is not even being executed.
Is it also best practice to pass this function reference through when rendering the template or to include it on the client?
EDIT
When looking in the markup, I can see that my JS is rendering correctly within the <script> of my template:
<script>
console.log("token: ", **token value**);
var setToken = function (token) {
console.log("::: setToken :::");
localStorage.setItem("cognitoToken", token);
};
setToken(**token value**)
</script>
EDIT 2
I have 4 pug pages that I load sequentially depending on the stage the user is at registering for Cognito. I have tested injecting JS into the other 3 templates and all of that code works fine. For example:
block scripts
script(type="text/javascript")
| var page = "confirm";
I can then call page in the console which prints out "confirm". I try the same variable declaration in my congrats.pug and returns undefined. So, I imagine, this has something to do with how I render the pages. Here's the comparison of 1 that works and the 1 that doesn't. I cannot see any difference here (extension of users.js):
/*
* Works
*/
router.post("/confirm", (req, res, next) => {
const { confirm } = req.body;
cognitoUser.confirmRegistration(confirm, true, function(err, result) {
if (err) {
next(err);
}
res.render("signin", {
title: "Signin"
});
});
});
//////////
/*
* Doesn't work
*/
router.post("/signin", (req, res) => {
const { username, password } = req.body;
const authenticationData = {
Username: username,
Password: password
};
const authenticationDetails = new AuthenticationDetails(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function(result) {
res.render("congrats", {
title: "Congrats",
token: result.getAccessToken().getJwtToken(),
setToken: setToken
});
},
onFailure: function(err) {
next(err);
}
});
});
//////////
In the end, for some reason it was the fact that I passed a function reference to my template the way I did. If I take that out, JS worked as it should.
Setting the token, I decided it was too much overhead & added complication to call a function in an external file so just did it directly within a <script> tag within the template. Here is my resulting working template:
extends layout
block scripts
script(type="text/javascript").
var token = "#{token}";
localStorage.setItem("cognitoToken", token);
block content
.container
.col-md-6.mx-auto.text-center
.header-title
h1.wv-heading--title= title
.row
.col-md-4.mx-auto
p You have successfully authenticated with AWS Cognito, your access token is
strong #{token}
Credit to this answer which showed me how to reference a pug param within a <script> tag

Configuring jsdom in Jest across multiple tests without using modules

I want to test scripts in an environment where we can not export modules. I have installed Jest version 23.1.0 and there aren't other packages in my package.json file.
Using jsdom 'old' api I have come up with a solution that works as expected:
script.js
var exVar = "test";
script.test.js
const jsdom = require('jsdom/lib/old-api.js');
test('old jsdom api config', function(done) {
jsdom.env({
html: "<html><body></body></html>",
scripts: [__dirname + "/script.js"],
done: function (err, window) {
expect(window.exVar).toBe("test");
done();
}
});
});
However with this implementation I have to re-write the config for every test, because it looks like the jsdom config gets re-written every time.
What I have tried
So far I have tried running this configuration:
const jsdom = require('jsdom/lib/old-api.js');
jsdom.env({
html: "<html><body></body></html>",
scripts: [__dirname + "/script.js"],
done: function (err, window) {
console.log('end');
}
});
with this test:
test('old jsdom api config', function(done) {
expect(window.exVar).toBe("test");
done();
});
in different ways: inside beforeAll, inside a script linked through setupFiles or through setupTestFrameworkScriptFile in the Jest configuration object, but still nothing works.
Maybe I could extend jest-environment as suggested in the docs, but I have no idea of the syntax I should be using, nor of how to link this file to the tests.
Thanks to my co-worker Andrea Talon I have found a way of using the same setup for different tests (at least inside the same file) using the 'Standard API' (not the 'old API').
Here is the complete test file.
const {JSDOM} = require("jsdom")
const fs = require("fs")
// file to test
const srcFile = fs.readFileSync("script.js", { encoding: "utf-8" })
// the window
let window
describe('script.js test', () => {
beforeAll((done) => {
window = new JSDOM(``, {
runScripts: "dangerously"
}).window
const scriptEl = window.document.createElement("script")
scriptEl.textContent = srcFile
window.document.body.appendChild(scriptEl)
done()
})
test('variable is correctly working', (done) => {
expect(window.exVar).toBe("test");
done()
})
})
Additional setup
In order to load multiple scripts I have created this function which accepts an array of scripts:
function loadExternalScripts (window, srcArray) {
srcArray.forEach(src => {
const scriptEl = window.document.createElement("script")
scriptEl.textContent = src
window.document.body.appendChild(scriptEl)
});
}
So instead of appending every single script to the window variable I can load them by declaring them at the top of the file like this:
// files to test
const jQueryFile = fs.readFileSync("jquery.js", { encoding: "utf-8" })
const srcFile = fs.readFileSync("lib.js", { encoding: "utf-8" })
and then inside the beforeAll function I can load them altogether like this:
loadExternalScripts(window, [jQueryFile, srcFile])

Categories