I wonder if it is possible with gulp-connect to serve some files from a different directory. Something like:
http://localhost:8080/index.html => root: '/root/app'
but
http://localhost:8008/js/main.js => from '/root/js/' not from 'root/app/js'
http://localhost:8008/css/main.css => from '/root/css/' not from 'root/app/css/'
You can pass a middleware function to gulp-connect that allows you to modify the request object and therefore rewrite request URLs:
gulp.task('serve', function() {
connect.server({
root: 'root',
middleware: function() {
return [ function(req, res, next) {
if (!/^\/(js|css)\/.*/.test(req.url)) {
req.url = '/app' + req.url;
}
next();
}];
}
});
});
In the above any path that starts with /js/ or /css/ will be passed through unchanged. Since our base folder is root that means a path like /js/main.js will resolve to root/js/main.js.
All other paths will be prepended with /app, meaning a path like /index.html will transparently resolve to root/app/index.html.
Instead of using custom logic as I did above, you can also use something like http-rewrite-middleware, which allows you to specify nginx-inspired rewrite expressions:
var rewrite = require('http-rewrite-middleware');
gulp.task('serve', function() {
connect.server({
root: 'root',
middleware: function() {
return [ rewrite.getMiddleware([
{ from: '^/js/(.*)$', to: '/js/$1' },
{ from: '^/css/(.*)$', to: '/css/$1' },
{ from: '^(.*)$', to: '/app/$1' }
])];
}
});
});
Related
I have an project set up and running with Webpack 5.28.0 and webpack-dev-server 4.11.1
Its all working nicely but I would like to be able to have the dev server write some files back to my project root. These are debug/log files that I'd like to save as JSON.
I'd also like this to be automatic, I don't want to have to click anything or trigger the action manually.
So the ideal flow would be that I run npm start, my build kicks off in a browser, the page generates a load of log data and this is then written back to my project root. Either using some browser function or calling back to Node script in my build.
Is this possible with dev-server?
You could setup the dev-server middleware to add an API endpoint to accept data and write it to your filesystem
// webpack.config.js
const { writeFile } = require("node:fs/promises");
const bodyParser = require("body-parser");
module.exports = {
// ...
devServer: {
setupMiddlewares: (middlewares, devServer) => {
devServer.app?.post(
"/__log",
bodyParser.json(),
async (req, res, next) => {
try {
await writeFile(
"debug-log.json",
JSON.stringify(req.body, null, 2)
);
res.sendStatus(202);
} catch (err) {
next(err);
}
}
);
return middlewares;
},
},
};
Then your front-end app needs only to construct the payload and POST it to the dev-server
const debugData = { /* ... */ };
fetch("/__log", {
method: "POST",
body: JSON.stringify(debugData),
headers: { "content-type": "application/json" },
});
I'm using the charting library from TradingView, which requires static HTML to be loaded inside an iFrame. I've placed the static html inside the public folder:
/public/charting_library/en-tv-chart.b555c6a4.html
And it is accessed via:
localhost:3000/charting_library/en-tv-chart.b555c6a4.html
However, when requesting the above URL, the contents are that of the root index.html, not that of the static asset.
How can I get Vite to route the HTML asset correctly here?
I solved this by using Vite middleware:
function chartingLibrary(): PluginOption {
return {
apply: 'serve',
configureServer(server: ViteDevServer) {
return () => {
server.middlewares.use(async (req, res, next) => {
if (req.originalUrl?.includes('/charting_library/') && req.originalUrl?.includes('html')) {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.write(fs.readFileSync(path.join(__dirname, `public/${req.originalUrl}`)));
res.end();
}
next();
});
};
},
name: 'charting-library',
};
}
And then in the config:
{
// ...
plugins: [
// ...
chartingLibrary(),
],
}
I downloaded my XML sitemap from the sitemap xml generator website. I placed my sitemap.xml on my public directory but when I tried to submit the sitemap.xml into google console i received the following error: General HTTP error: 404 not found
HTTP Error: 404So i codedapp.get('/sitemap.xml', function( req, res, next ) {
res.header('Content-Type', 'text/xml');
res.render( 'sitemap' );
)};And when i navigate to the 'website/sitemap.xml' I am getting the following error: This page contains the following errors:
error on line 1 at column 42: Specification mandate value for attribute itemscope
Thanks for your help
Generate your sitemap.xml file using a tool like https://www.xml-sitemaps.com/
upload the sitemap.xml in your project
then add this to your .js file:
router.get('/sitemap.xml', function(req, res) {
res.sendFile('YOUR_PATH/sitemap.xml');
});
make sure you change YOUR_PATH for the actual path where your sitemap.xml file is.
Sitemaps do not have to be XML documents. A simple text file with URLs is all you need so something like below works fine. In the following example, fetchMyUrls() would be a function/method that asynchronously gets and returns the available URLs as an array of strings (URL strings).
async function index (req, res){
return fetchMyUrls().then((urls) => {
var str = '';
for (var url of urls) {
str = str + url + '\n';
}
res.type('text/plain');
return res.send(str);
});
}
For those looking for a way to create the XML dynamically on your code and don't want to use another library nor have a file stored in the public folder, you can use this:
app.get('/sitemap.xml', async function(req, res, next){
let xml_content = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
' <url>',
' <loc>http://www.example.com/</loc>',
' <lastmod>2005-01-01</lastmod>',
' </url>',
'</urlset>'
]
res.set('Content-Type', 'text/xml')
res.send(xml_content.join('\n'))
})
In my NodeJS express project and without installing any library I was able to add this to my routes with my preferred view engine (handlebar).
export const routes: RouteMapper[] = [
{
"/sitemap.xml": [
{
method: "get",
handler: (req, res) =>
res.sendFile("/src/views/sitemap.xml", { root: "." }),
},
],
},
];
Cheers!
The best way is to create a script that would automatically generate a sitemap. In a lot of cases, the URLs should be dynamic based on data from the database.
Great package for creating the sitemap in Express is sitemap package:
STEP 1
Create a middleware that will generate the sitemap dynamically and then cache it for each next call to the server. We can extract logic in separate file called sitemap_generator.js for example, and we can define and export generate_sitemap middleware for it:
const { SitemapStream, streamToPromise } = require('sitemap');
const { Readable } = require('stream');
let sitemap;
const generate_sitemap = async (req, res, next) => {
res.header('Content-Type', 'application/xml');
if (sitemap) return res.status(200).send(sitemap); // If we have a cached entry send it
let changefreq = 'weekly';
try {
let links = [
{ url: '', changefreq, priority: 1 },
{ url: 'aboutus', changefreq, priority: 0.9 },
{ url: 'blog', changefreq },
{ url: 'login', changefreq },
{ url: 'register', changefreq },
];
// Additionally, you can do database query and add more dynamic URLs to the "links" array.
const stream = new SitemapStream({ hostname: 'https://example.com', lastmodDateOnly: true })
return streamToPromise(Readable.from(links).pipe(stream)).then((data) => {
sitemap = data; // Cache the generated sitemap
stream.end();
return res.status(200).send(data.toString())
});
} catch (error) {
return res.status(500).end();
}
}
module.exports = { generate_sitemap };
STEP 2
Import generate_sitemap middleware from sitemap_generator.js in your server configuration file and mound it to the /sitemap.xml endpoint:
const { generate_sitemap } = require('./sitemap_generator');
...
app.get('/sitemap.xml', generate_sitemap);
That's it. Your sitemap should be available on /sitemap.xml endpoint now so navigate in the browser to that endpoint and check if it is there.
I'm trying to use grunt serve with the connect-rest middleware. I tried to configure connect-rest in my Gruntfile.js which worked for GET requests, but not for POST. I wonder what I'm missing out. This is my Gruntfile.js (excerpt):
module.exports = function (grunt) {
var postResponder = function(request, content, callback) {
callback(null, {Status : 42});
}
var Rest = require('connect-rest');
var rest = Rest.create( {context: '/'} );
rest.assign(['get'],'/*', postResponder);
...
livereload: {
options: {
open: true,
middleware: function (connect) {
return [
connect().use(rest.processRequest()),
connect.static('.tmp'),
connect().use('/bower_components', connect.static('./bower_components')),
connect().use('/app/styles', connect.static('./app/styles')),
connect.static(appConfig.app)
];
}
}
},
That works like a charm. But when I change that one line to:
rest.assign(['post'],'/*', postResponder);
or
rest.assign('*','/*', postResponder);
Then on posting I get the following entry in the log (running grunt --debug):
[D] server POST /api/groups.json?cache=false&force=true&requesttime=2015-10-29T06:46:24.443Z 404 103 - 2.027 ms
and a 404 error when posting (get works).
What do I miss?
It turned out, that for my purpose - being able to support post of JSON as a mock I didn't need connect-rest in the first place. The build in capabilities of connect provided all I needed. So the start of the Gruntfile.js now looks like this:
module.exports = function (grunt) {
var bodyParser = require("body-parser");
var postResponder = function(request, response, next) {
if (request.method === 'POST') {
console.log(request.method+" "+request.url);
response.setHeader('Content-Type', 'application/json');
response.statusCode = 200;
response.end(JSON.stringify(request.body));
} else {
next();
}
};
and the livereload section like this:
livereload: {
options: {
open: true,
middleware: function (connect) {
return [
connect().use('/api', bodyParser.json()),
connect().use('/api', postResponder),
connect.static('.tmp'),
connect().use('/bower_components',
connect.static('./bower_components')),
connect().use('/app/styles', connect.static('./app/styles')),
connect.static(appConfig.app)
];
}
}
}
Note: I limit postbacks to calls to /api here - you might need different endpoints. The example simply echos back the JSON received.
All of the Hapi examples (and similar in Express) shows routes are defined in the starting file:
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({ port: 8000 });
server.route({
method: 'GET',
path: '/',
handler: function (request, reply) {
reply('Hello, world!');
}
});
server.route({
method: 'GET',
path: '/{name}',
handler: function (request, reply) {
reply('Hello, ' + encodeURIComponent(request.params.name) + '!');
}
});
server.start(function () {
console.log('Server running at:', server.info.uri);
});
However, it's not hard to image how large this file can grow when implementing production application with a ton of different routes. Therefore I would like to break down routes, group them and store in separate files, like UserRoutes.js, CartRoutes.js and then attach them in the main file (add to server object). How would you suggest to separate that and then add?
You can create a separate file for user routes (config/routes/user.js):
module.exports = [
{ method: 'GET', path: '/users', handler: function () {} },
{ method: 'GET', path: '/users/{id}', handler: function () {} }
];
Similarly with cart. Then create an index file in config/routes (config/routes/index.js):
var cart = require('./cart');
var user = require('./user');
module.exports = [].concat(cart, user);
You can then load this index file in the main file and call server.route():
var routes = require('./config/routes');
...
server.route(routes);
Alternatively, for config/routes/index.js, instead of adding the route files (e.g. cart, user) manually, you can load them dynamically:
const fs = require('fs');
let routes = [];
fs.readdirSync(__dirname)
.filter(file => file != 'index.js')
.forEach(file => {
routes = routes.concat(require(`./${file}`))
});
module.exports = routes;
You should try Glue plugin: https://github.com/hapijs/glue. It allows you to modularize your application. You can place your routes in separate subdirectories and then include them as Hapi.js plugins. You can also include other plugins (Inert, Vision, Good) with Glue as well as configure your application with a manifest object (or json file).
Quick exapmple:
server.js:
var Hapi = require('hapi');
var Glue = require('glue');
var manifest = {
connections: [{
port: 8080
}],
plugins: [
{ inert: [{}] },
{ vision: [{}] },
{ './index': null },
{
'./api': [{
routes: {
prefix: '/api/v1'
}
}]
}
]
};
var options = {
relativeTo: __dirname + '/modules'
};
Glue.compose(manifest, options, function (err, server) {
server.start(function(err) {
console.log('Server running at: %s://%s:%s', server.info.protocol, server.info.address, server.info.port);
});
});
./modules/index/index.js:
exports.register = function(server, options, next) {
server.route({
method: 'GET',
path: '/',
handler: require('./home')
});
});
exports.register.attributes = {
pkg: require('./package.json')
};
./modules/index/package.json:
{
"name": "IndexRoute",
"version": "1.0.0"
}
./modules/index/home.js:
exports.register = function(req, reply) {
reply.view('home', { title: 'Awesome' });
});
Have a look at this wonderful article by Dave Stevens for more details and examples.
You can use require-hapiroutes to do some of the organization and loading for you. (I am the author so I am a little biased, I wrote it to make my life easier in managing routes)
I am a big fan of require-directory and and wanted a way to manage my routes just as easily. This lets you mix and match routes in your modules and modules in directories with routes.
You can then do something like this...
var routes = require('./routes');
server.route(routes.routes);
Then in your directory you could have a route file like...
module.exports = [
{
method : 'GET',
path : '/route1',
handler : routeHandler1,
config : {
description: 'my route description',
notes: 'Important stuff to know about this route',
tags : ['app']
}
},
{
method : 'GET',
path : '/route2',
handler : routeHandler2,
config : {
description: 'my route description',
notes: 'Important stuff to know about this route',
tags : ['app']
}
}];
Or, you can mix and match by assigning to a "routes" property on the module
module.exports.routes = [
{
method : 'GET',
path : '/route1',
handler : routeHandler1,
config : {
description: 'my route description',
notes: 'Important stuff to know about this route',
tags : ['app']
}
},
{
method : 'GET',
path : '/route2',
handler : routeHandler2,
config : {
description: 'my route description',
notes: 'Important stuff to know about this route',
tags : ['app']
}
}];
Always, good to have options. There is full documentation on the github or npmjs site for it.
or you can use a index file to load all the routes
in the directory
index.js
/**
* Module dependencies.
*/
const fs = require('fs');
const path = require('path');
const basename = path.basename(__filename);
const routes = fs.readdirSync(__dirname)
.filter((file) => {
return (file.indexOf('.') !== 0) && (file !== basename);
})
.map((file) => {
return require(path.join(__dirname, file));
});
module.exports = routes;
other files in the same directory like:
module.exports = [
{
method: 'POST',
path: '/api/user',
config: {
}
},
{
method: 'PUT',
path: 'api/user/{userId}',
config: {
}
}
];
and than in your root/index
const Routes = require('./src/routes');
/**
* Add all the routes
*/
for (var route in Routes) {
server.route(Routes[route]);
}
Interesting to see so many different solutions, here is another one.
Globbing to the rescue
For my latest project I settled on globbing for files with a particular name pattern and then requiring them into the server one by one.
Import routes after having created the server object
// Construct and setup the server object.
// ...
// Require routes.
Glob.sync('**/*route*.js', { cwd: __dirname }).forEach(function (ith) {
const route = require('./' + ith);
if (route.hasOwnProperty('method') && route.hasOwnProperty('path')) {
console.log('Adding route:', route.method, route.path);
server.route(route);
}
});
// Start the server.
// ...
The glob pattern **/*route*.js will find all files within and below the specified current working directory with a name that contains the word route and ends with the suffix .js.
File structure
With the help of globbing we have a loose coupling between the server object and its routes. Just add new route files and they will be included the next time you restart your server.
I like to structure the route files according to their path and naming them with their HTTP-method, like so:
server.js
routes/
users/
get-route.js
patch-route.js
put-route.js
articles/
get-route.js
patch-route.js
put-route.js
Example route file routes/users/get-route.js
module.exports = {
method: 'GET',
path: '/users',
config: {
description: 'Fetch users',
// ...
},
handler: function (request, reply) {
// ...
}
};
Final thoughts
Globbing and iterating over files is not a particularly fast process, hence a caching layer may be worth investigating in production builds depending on your circumstances.
Try hapi-auto-route plugin! It's is very simple to use and allow prefix in your route path.
Full disclosure: I am the author of this plugin
I know this is already approved. I put down my solution in case someone wants a quick fix and new to Hapi.
Also I included some NPM too so Newbees can see how to to use the server.register with multiple plugin in the case ( good + hapi-auto-route )
Installed some npm packages:
npm i -S hapi-auto-route
npm i -S good-console
npm i -S good
// server.js
'use strict';
const Hapi = require('hapi');
const Good = require('good');
const AutoRoute = require('hapi-auto-route');
const server = new Hapi.Server();
server.connection(
{
routes: { cors: true },
port: 3000,
host: 'localhost',
labels: ['web']
}
);
server.register([{
register: Good,
options: {
reporters: {
console: [{
module: 'good-squeeze',
name: 'Squeeze',
args: [{
response: '*',
log: '*'
}]
}, {
module: 'good-console'
}, 'stdout']
}
}
}, {
register: AutoRoute,
options: {}
}], (err) => {
if (err) {
throw err; // something bad happened loading the plugin
}
server.start((err) => {
if (err) {
throw err;
}
server.log('info', 'Server running at: ' + server.info.uri);
});
});
In your routes/user.js
module.exports =
[
{
method: 'GET',
path: '/',
handler: (request, reply) => {
reply('Hello, world!');
}
},
{
method: 'GET',
path: '/another',
handler: (request, reply) => {
reply('Hello, world again!');
}
},
];
Now run: node server.js
Cheers