Insert different scripts based on environment in Next.js at runtime - html

I want to insert a script in my application's <head>, but the src attribute of that script depends on a runtime environment variable in my hosting server (OpenShift).
So if process.env.ENVIRONMENT === "test", I want to insert <script src="www.someurl.com/test.js" /> into the application's <head> and if it's "prod" then something else.
I have all these src values stored in a json file that I don't want to show up in the client. How do I make sure the client can receive the correct src from the server without having access to the json file with all the environment's endpoints?
Express:
With Express I used to just inject the src value into the window object at runtime before serving index.html but I'm not sure how to do it in Next.js
Code: Here's what I tried
// _app.tsx
...
<Head>
<script src={getScript()} type="text/javascript" />
</Head>
...
where getScript() is a function in <root>/scripts
export function getScripts() {
if (process.env.ENVIRONMENT === "test") {
return scriptSrc.test;
} else if (process.env.ENVIRONMENT === "prod") {
return scriptSrc.prod;
}
}

You can use getServerSideProps to pass properties from the server to the client. Since the server has access to any runtime env variables, you can simply pass your variable to the client. In the render function use the env variable to decide which script to inject into the header.
This simple page showcases how to pass an env variable:
export const getServerSideProps = async () => {
return {
props: {
my_env: process.env.RUNTIME_ENV || 'default',
}
}
}
export default function MyPage(props) {
return <div>{props.my_env}</div>
}
npm run dev renders "default" while RUNTIME_ENV="foobar" npm run dev renders "foobar".

I couldn't find a way to populate the data in _app.js at fetch time from the server since that file doesn't support getServerSideProps so I took the long road and did it client side instead.
Step 1:
Create an api route that returns back a JSON object with all the data you need in all the pages of the application. All process.env references work inside the pages/api folder since that code always runs on the server.
export default (
req: NextApiRequest,
res: NextApiResponse<StartResponseType>
) => {
res.status(200).json(getScripts(process.env.ENVIRONMENT.trim()));
};
Step 2:
Now we need an efficient way to cache this response on the client since we don't want to keep fetching it for every page. I used react-query for this.
export function queryStart() {
return useQuery<StartResponseType>("start", getStart, {
staleTime: Infinity,
});
}
Step 3:
We can't use the hook created above in _app.js until we wrap our App component in QueryClientProvider, so we need to create a Higher Order Component since there's no component that wraps App.
export const withQueryCache = (Page: any) => (props: any) => (
<QueryCacheProvider>
<Page {...props} />
</QueryCacheProvider>
);
Step 4:
Now we just need to put it all together and add a loading state to the App so it only initiates the application once the initial data has been fetched.
// _app.tsx
function App({ Component, pageProps }: AppProps) {
const startQuery = queryStart();
if (startQuery.isSuccess) {
return (
<>
<Head>
<script src={startQuery.data.scriptEndpoint} type="text/javascript" />
</Head>
<Component {...pageProps} />
</>
);
} else return <>Loading</>;
}
export default withQueryCache(App);
However, I would've still preferred for a way to just inject data from the server at fetch time. That would've been possible if _app.js file supported getServerSideProps. But until that's supported, this seems to be the best workaround.

Related

Nextjs/Vercel error: only absolute URLs are supported. Where is the '.json' in my route params coming from?

The Problem:
I'm new to Next.js (1 month) and Vercel (1 day), and between them something appears to be inserting .json in my urls on the search route, causing them to fail with error:
[GET] /_next/data/9MJcw6afNEM1L-eax6OWi/search/hand.json?term=hand
10:21:52:87
Function Status:None
Edge Status:500
Duration:292.66 ms
Init Duration: 448.12 ms
Memory Used:88 MB
ID:fra1:fra1::ldzhz-1644484912454-0a30b71b6c90
User Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
TypeError: Only absolute URLs are supported
at getNodeRequestOptions (/var/task/node_modules/next/dist/compiled/node-fetch/index.js:1:61917)
at /var/task/node_modules/next/dist/compiled/node-fetch/index.js:1:63448
at new Promise (<anonymous>)
at Function.fetch [as default] (/var/task/node_modules/next/dist/compiled/node-fetch/index.js:1:63382)
at fetchWithAgent (/var/task/node_modules/next/dist/server/node-polyfill-fetch.js:38:39)
at getServerSideProps (/var/task/.next/server/chunks/730.js:238:28)
at Object.renderToHTML (/var/task/node_modules/next/dist/server/render.js:566:26)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async doRender (/var/task/node_modules/next/dist/server/base-server.js:855:38)
at async /var/task/node_modules/next/dist/server/base-
server.js:950:28
2022-02-10T09:21:53.788Z 994c9544-0bbe-4a68-af83-f0e4c322151e ERROR
Error: Your `getServerSideProps` function did not return an object. Did you forget to add a `return`?
at Object.renderToHTML (/var/task/node_modules/next/dist/server/render.js:592:19)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async doRender (/var/task/node_modules/next/dist/server/base-server.js:855:38)
at async /var/task/node_modules/next/dist/server/base-server.js:950:28
at async /var/task/node_modules/next/dist/server/response-cache.js:63:36 { page: '/search/[term]'}
RequestId: 994c9544-0bbe-4a68-af83-f0e4c322151e Error: Runtime exited with error: exit status 1
Runtime.ExitError
Though the browser says https://.../search/hand as it should.
No such thing is happening on my local server build though, and it works perfectly well.
Background/Code Snippets:
The search route is the only route that uses SSR, and is also the only route with this issue. It is a dynamic route, so it seems either next in production or vercel expects some kind of json for it -presumably pre-rendered content-, and is replacing the route URL with json.
Also I have had to use the VERCEL_URL environment variable to prepare a URL for fetch requests, so this may also be messing up the URL, but the .json in the error message makes me think otherwise, since search should not be pre-rendered.
The Page Structure For the Search Route (Index imports the component in [term] and defines its own getServerSide props to accommodate a search route without a param):
|-Search
|- [term].js
|- Index.js
The Code For [term].js:
...
export default function Search({results, currentSearch}){
...
}
export async function getServerSideProps(req) {
const { criteria, page } = req.query;
const { term } = req.params || { term: '' };
try {
const data = await fetch(`${process.env.VERCEL_URL}/api/search/${term}?criteria=${criteria || 'name'}&page=${page}`);
const searchRes = await data.json();
return {
props: {
results: searchRes.data,
currentSearch: searchRes.query
}
}
} catch (e) {
console.log(e)
}
}
Index.js is similar:
import Search from "./[term]";
export default Search;
export async function getServerSideProps(req) {
const { criteria, page } = req.query;
const { term } = req.params || { term: '' };
if(!term){
return {
props: {
results: [],
currentSearch: {}
}
}
}
try {
const data = await fetch(`${process.env.VERCEL_URL}/api/search/${term}?criteria=${criteria || 'name'}&page=${page}`);
const searchRes = await data.json();
return {
props: {
results: searchRes.data,
currentSearch: searchRes.query
}
}
} catch (e) {
console.log(e)
}
}
The API I'm trying to fetch from is confirmed to be working, so this problem is strictly regarding pages, or .json being provided to the fetch method from router params.
It would turn out that VERCEL_URL is actually an absolute URL (It does not include a protocol). I had to deploy console.log statements to find this. A little embarrassed that I missed it in the docs.
The .json was not actually in the query or params, and therefore not in the fetch request. The fetch failed because the url had no protocol.
The .json in the page url must be from Next's internal operations, and does not mean the page is being built ahead of time. Yes it is being rendered using some json, but my thinking that the json indicates a pre-rendered page(SSG/ISR) was wrong. This must mean Server Side Rendering will also make use of such json, but only at runtime, when the request is made.
The use of .json after the params slug in the GET requests for a page has no bearing on the internal flow of your app, provided it has worked correctly. If you see it in error messages, know that it is from Next and examine other parts of the code at the point of failure.
The page structure I attempted ([param].js + index.js in the same
directory) is fine, which is why my local build could work properly.
I want to delete this question because the solution is essentially one that a thorough look in the docs would have revealed, but at the same time I think the mistake itself is an easily made one and that some of the conclusions listed above(particularly the one about json being used in all next routes) could save time spent debugging for some new users of Next/Vercel.

Composable functions in Puppeteers page.Evaluate

I'm relatively new to puppeteer and I'm trying to understand the patterns that can be used to build more complex apis with it. I am building a cli where I am running a WebGL app in puppeteer which i call various functions in, and with my current implementation i have to copy and paste a lot of setup code.
Usually, in every cli command i have to setup pupeteer, setup the app and get access to its api object, and then run an arbitrary command on that api, and get the data back in node.
It looks something like this.
const {page, browser} = await createBrowser() // Here i setup the browser and add some script tags.
let data;
page.exposeFunction('extractData', (data) => {
data = data;
})
await page.evaluate(async (input) => {
// Setup work
const requestEvent = new CustomEvent('requestAppApi', {
api: undefined;
})
window.dispatchEvent(requestEvent);
const api = requestEvent.detail.api;
// Then i call some arbitrary function, that will
always return some data that gets extracted by the exposed function.
const data = api.arbitraryFunction(input);
window.extractData(data)
}, input)
What i would like is to wrap all of the setup code in a function, so that i could call it and just specify what to do with the api object once i have it.
My initial idea was to have a function that will take a callback that has this api object as a parameter.
const { page, browser } = wait createBrowser();
page.exposeFunction(async (input) =>
setupApiObject(((api) =>
api.callSomeFunction(input)
), input)
However, this does not work. I understand that puppeteer requires any communication between the node context and the browser to be serialised as json, and obviously a function cant be. Whats tripping me up is that I'm not actually wanting to call these methods in the node context, just have a way to reuse them. The actual data transfer is already handled by page.exposeFunction.
How would a more experienced puppeteer dev accomplish this?
I'll answer my own question here, since i managed to figure out a way to do it. Basically, you can use page.evaluate to create a function on the window object that can later be reused.
So i did something like
await page.evaluate(() => {
window.useApiObject = function(callback: (api) => void){
// Perform setup code
callback()
}
})
Meaning that later on i could use that method in the browser context and avoid redoing the setup code.
page.evaluate(() => {
window.useApiObject((api) => {
api.someMethod()
})
})

how can I set a global function for call Apis in NUXT?

after some google searched,
I am doing this in plugins/callApi.js
import axios from 'axios';
import Vue from 'vue';
Vue.use(callApi);
export default async (apiUrl, postData, headers = {}) => {
let msg = await axios.post(https://my-api-location.com' + apiUrl,postData);
return msg.data;
};
then set in nuxt.config.js
plugins: [
'~plugins/callApi.js']
when I want to use it
const msgData = await callApi('/api/news/getNewsList', postData);
if (msgData.response === 'success') { .......
but when I start yarn dev, it hightlight "Vue.use(callApi);" part and says "callApi is not defined"
how cant I fix it? thanks a lot :)
I actually re-read your answer because I was a little confused.
So, in your callApi.js, you define what you want there, but you are calling Vue.use(callApi) in the file where you are actually defining what callApi will be.
At the moment the compiler goes to Vue.use(callApi), this variable "callApi" is not defined yet (because it will only be available after it finishes compiling this very file).
So just do the
import axios from 'axios';
import Vue from 'vue';
export default async (apiUrl, postData, headers = {}) => {
let msg = await axios.post(https://my-api-location.com' + apiUrl,postData);
return msg.data;
};
then, by setting the plugin path on the plugins property in the nuxt.config file (exactly the way you did), the very callApi file will be automatically called (try to put a console log in your callApi file, and you'll see it logging when you start your application).
An example of what you want:
https://codesandbox.io/s/nuxt-app-demovue-ssr-forked-s94rz?file=/pages/index.vue
Now in your plugin, you have to decide exactly what you want to do. you might want to expose the function or make it globally available, that's up to you!
For this last option, you might want to take a look here:
Vue/Nuxt: How to define a global method accessible to all components?
Good luck!

vuejs: the correct path of local json file for axios get request

In my Vue project, I have mocked some data for next step development. I already save the test data in a json file. And my vue project is typical one created with Vue-Cli, and the structure for my project goes as following:
My_project
build
config
data
service_general_info.json
node_modules
src
components
component-A
component-A.vue
as you can see, all the folders are created by the vue-cli originally. And I make a new folder data and place the test data json file inside.
And I want to read in the data by axios library in an event handling function inside the component of component-A as following:
methods: {
addData() {
console.log('add json data...');
axios.get('./../../data/service_general_info.json');
},
},
I use relative path to locate the target file.But get 404 error back. So how to set the path correctly? Currently I am running the dev mode in local host.
The error message is: GET http://localhost:8080/data/service_general_info.json 404 (Not Found)
In Vue-cli project, axios can't get data from custom folder.
You should use static folder to save test json file.
So you should change axios call like this:
axios.get('/static/service_general_info.json');
This will get data from json.
If you are doing just for sake of testing then you can save it in public folder and access it directly on http root.
e.g. I have the file results.json in public folder then I can access it using http://localhost:8080/results.json
For me it didn't work using static folder. I had to put it in public folder.
I put json folder in public & then accessed it like below.
getCountries() {
return axios.get('json/country-by-abbreviation.json', { baseURL: window.location.origin })
.then((response) => { return response.data; })
.catch((error) => {
throw error.response.data;
});
}
When the http call is made from the server, axios has no idea that you're on http://localhost:8080, you have to give the full url.
Like this:
methods: {
addData() {
console.log('add json data...');
axios.get('http://localhost:8080/data/service_general_info.json');
},
},
I had this same issue, only the above solutions wouldn't work as it is being uploaded to a subdirectory. I found I needed to put it in the public/assets folder and use:
axios.get(process.env.BASE_URL+'assets/file.json')
While in vue.config.js I have set the local and live paths
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/path/to/app/'
: '/'
}
You can simply read a static JSON file using import. Then assign in data.
import ServiceInfo from './../../data/service_general_info.json';
export default{
data(){
return {
ServiceInfo
}
}
}

Loading JSON Without an HTTP Request

I am working on a project using Angular 4, NPM, Node.js, and the Angular CLI.
I have a rather unusual need to load JSON into an Angular service (using an #Injectable) without an HTTP request, i.e. it will always be loaded locally as part of the package, and not retrieved from a server.
Everything I've found so far indicates that you either have to modify the project's typings.d.ts file or use an HTTP request to retrieve it from the /assets folder or similar, neither of which is an option for me.
What I am trying to accomplish is this. Given the following directory structure:
/app
/services
/my-service
/my.service.ts
/myJson.json
I need the my.service.ts service, which is using #Injectable, to load the JSON file myJson.json. For my particular case, there will be multiple JSON files sitting next to the my.service.ts file that will all need to be loaded.
To clarify, the following approaches will not work for me:
Using an HTTP Service to Load JSON File From Assets
URL: https://stackoverflow.com/a/43759870/1096637
Excerpt:
// Get users from the API
return this.http.get('assets/ordersummary.json')//, options)
.map((response: Response) => {
console.log("mock data" + response.json());
return response.json();
}
)
.catch(this.handleError);
Modifying typings.d.ts To Allow Loading JSON Files
URL: https://hackernoon.com/import-json-into-typescript-8d465beded79
Excerpt:
Solution: Using Wildcard Module Name
In TypeScript version 2 +, we can use wildcard character in module name. In your TS definition file, e.g. typings.d.ts, you can add this line:
declare module "*.json" {
const value: any;
export default value;
}
Then, your code will work like charm!
// TypeScript
// app.ts
import * as data from './example.json';
const word = (<any>data).name;
console.log(word); // output 'testing'
The Question
Does anyone else have any ideas for getting these files loaded into my service without the need for either of these approaches?
You will get an error if you call json directly, but a simple workaround is to declare typings for all json files.
typings.d.ts
declare module "*.json" {
const value: any;
export default value;
}
comp.ts
import * as data from './data.json';
The solution I found to this was using RequireJS, which was available to me via the Angular CLI framework.
I had to declare require as a variable globally:
declare var require: any;
And then I could use require.context to get all of the files in a folder I created to hold on the types at ../types.
Please find below the entire completed service that loads all of the JSON files (each of which is a type) into the service variable types.
The result is an object of types, where the key for the type is the file name, and the related value is the JSON from the file.
Example Result loading files type1.json, type2.json, and type3.json from the folder ../types:
{
type1: {
class: "myClass1",
property1: "myProperty1"
},
type2: {
class: "myClass2",
property1: "myProperty2"
},
type3: {
class: "myClass3",
property1: "myProperty3"
}
}
The Final Service File
import { Injectable } from '#angular/core';
declare var require: any;
#Injectable()
export class TypeService {
constructor(){
this.init()
};
types: any;
init: Function = () => {
// Get all of the types of branding available in the types folder
this.types = (context => {
// Get the keys from the context returned by require
let keys = context.keys();
// Get the values from the context using the keys
let values = keys.map(context);
// Reduce the keys array to create the types object
return keys.reduce(
(types, key, i) => {
// Update the key name by removing "./" from the begining and ".json" from the end.
key = key.replace(/^\.\/([^\.]+)\.json/, (a, b)=> { return b; });
// Set the object to the types array using the new key and the value at the current index
types[key] = values[i].data;
// Return the new types array
return types;
}, {}
);
})(require.context('../types', true, /.json/));
}
}
You can directly access variables in services from their object that is defined in the constructor.
...So say your constructor loads the service like this
constructor(private someService:SomeService){}
You can just do someService.theJsonObject to access it.
Just be careful not to do this before it gets loaded by the service function that loads it. You'd then get a null value.
You can assign variables to your service files the same way you do in component files.
Just declare them in the service
public JsonObject:any;
And (easiest way) is to let the function that called your service assign the JSON object for you.
So say you called the service like this
this.serviceObject.function().subscribe
(
resp =>
{
this.serviceObject.JsonObject = resp;
}
);
After this is done once, other components can access that JSON content using someService.theJsonObject as discussed earlier.
In your case I think all you need to do is embed your JSON object in your code. Maybe you can use const. That's not bad code or anything.