Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow serving one file for all urls (single page app mode) #204

Closed
wiemann opened this issue Jul 15, 2014 · 70 comments
Closed

Allow serving one file for all urls (single page app mode) #204

wiemann opened this issue Jul 15, 2014 · 70 comments

Comments

@wiemann
Copy link

wiemann commented Jul 15, 2014

Hi,
as many developers nowadays use Backbone, AngularJS etc. to build single page applications that consist of e.g. a /index.html page + pushstate routing, it would be great to have an option to make browser-sync serve one file. ("404/catchall/fallback" page you might also call it).

Maybe an option called {cachall: "/filename.html"} ?

@zckrs
Copy link

zckrs commented Jul 19, 2014

👍

2 similar comments
@lucasmotta
Copy link

+1

@ptim
Copy link

ptim commented Aug 7, 2014

+1

@lucasmotta
Copy link

Using a middleware I manage to solve this problem. This is the example that I'm using (specific for my case):

var fs = require("fs"),
        path = require("path"),
        url = require("url"),
        gulp = require("gulp"),
        browserSync = require("browser-sync");

// The default file if the file/path is not found
var defaultFile = "index.html"

// I had to resolve to the previous folder, because this task lives inside a ./tasks folder
// If that's not your case, just use `__dirname`
var folder = path.resolve(__dirname, "../");

gulp.task("server", function() {

    browserSync({
        files: ["./css/*.css", "./js/*.js", "./index.html"],
        server: {
            baseDir: "./",
            middleware: function(req, res, next) {
                var fileName = url.parse(req.url);
                fileName = fileName.href.split(fileName.search).join("");
                var fileExists = fs.existsSync(folder + fileName);
                if (!fileExists && fileName.indexOf("browser-sync-client") < 0) {
                    req.url = "/" + defaultFile;
                }
                return next();
            }
        }
    });

});

@lucasmotta
Copy link

Or if you are using coffee files:

fs          = require("fs")
path        = require("path")
url         = require("url")
gulp        = require("gulp")
browserSync = require("browser-sync")

# The default file if the file/path is not found
defaultFile = "index.html"

# I had to resolve to the previous folder, because this task lives inside a ./tasks folder
# If that's not your case, just use `__dirname`
folder = path.resolve(__dirname, "../")

gulp.task "server", ->
  browserSync
    files: ["./css/*.css", "./js/*.js", "./index.html"]
    server:
      baseDir: "./"
      middleware: (req, res, next) ->
        fileName = url.parse(req.url)
        fileName = fileName.href.split(fileName.search).join("")
        fileExists = fs.existsSync(folder + fileName)
        if not fileExists and fileName.indexOf("browser-sync-client") < 0
            req.url = "/#{defaultFile}"
        next()

@carlholloway
Copy link

@lucasmotta thanks for the snippet! A little modification to suit my needs + it works great

+1 for native support in browser sync

@subblue
Copy link

subblue commented Aug 24, 2014

Thanks @lucasmotta you solution works well for me too.
It would be great to have this as a standard option of course +1

@miickel
Copy link

miickel commented Sep 2, 2014

@lucasmotta thanks and high-five! ✋

@shakyShane
Copy link
Contributor

I'm a little bit reluctant to add things to core unless they are used by 70/80%+ of use-cases...

@lucasmotta - perhaps you could publish your middleware as a tiny package on NPM - then I can add some documentation advising people to use it...

@armandabric
Copy link

I didn't know if this is better to natively manage this case or distribute an NPM package. But this is a must have feature.

@lucasmotta Thank for the middleware 👍

@shakyShane I'm not so sure that so few people will be interested by this. This is not an isolated use case.

@morrislaptop
Copy link
Contributor

+1

@wiemann wiemann changed the title Allow serving one file for all urls (sigle page app mode) Allow serving one file for all urls (single page app mode) Sep 12, 2014
@Timopheym
Copy link

@lucasmotta thanks you a lot!

@JoshSGman
Copy link

@lucasmotta That did it for me too thanks!

@bnetter
Copy link

bnetter commented Sep 19, 2014

+1

@bnetter
Copy link

bnetter commented Sep 19, 2014

Would be great to just have an option on the server. Something like:

server: {
  {
    always: "index.html"
  }
}

@shakyShane
Copy link
Contributor

ok, this is getting some momentum now.

I've not needed this myself, so forgive my ignorance - but are we literally just talking about that middleware that @lucasmotta posted above?

and would always be a good option name for this?

@bnetter
Copy link

bnetter commented Sep 19, 2014

The need for this comes from Single Page Application: routing is done on the client side. So you always deliver index.html and a javascript framework (AngularJS or EmberJS for example) handles the routing.

@bnetter
Copy link

bnetter commented Sep 19, 2014

@lucasmotta example works great. Another option could be {spa: true} or {spa: 'index.html'}.

@shakyShane
Copy link
Contributor

👍 for spa - anyone else?

@carlholloway
Copy link

(Obviously images, js/css files and other static content still need to be served up normally.)
spa sounds good... catchAll was suggested earlier and also isn't a bad name.

@armandabric
Copy link

You need to configure some static path to allow serve image/css...

@bnetter
Copy link

bnetter commented Sep 19, 2014

The script actually looks for files before serving index.html.

@ben-lin
Copy link

ben-lin commented Sep 21, 2014

so do we have the spa mode yet?

@marcgibbons
Copy link

Tried using connect-modrewrite as a middleware?
#26

@mcranston18
Copy link

+1 @marcgibbons

@Shahor
Copy link

Shahor commented Oct 24, 2014

👍 up for this

I have a repo of tasks that I require on my projects when I need them. If I have to use the middleware with this I have to resolve potentially complicate paths, which I don't want really want to :/

Also this is a pretty important feature for SPA imho

@adamalbrecht
Copy link

The connect-history-api-fallback package also seems to work:

historyApiFallback = require('connect-history-api-fallback')

server: {
  baseDir: "...",
  middleware: [ historyApiFallback() ]
}

@lukejacksonn
Copy link

Does anyone know if if there is a CLI equivalent to middleware: [ historyApiFallback() ].. I have been using this in my gulp task and it works great but recently I've been trying to take advantage of npm scripts and can't figure out if this configuration can be achieved through the command line.

I'm thinking something along the lines of:
browser-sync start --server dist --middleware historyApiFallback
or if historyApiFallback was supported by browser-sync out of the box..
browser-sync start --app dist

@shakyShane
Copy link
Contributor

@lukejacksonn just create a file called bs.js like

var browserSync = require("browser-sync").create();

browserSync.init({
    files: ["app/css/*.css"],
    server: {
        baseDir: "app",
        middleware: [ historyApiFallback() ]
    }
});

then in your npm script, just do "serve": "node bs.js"

@lukejacksonn
Copy link

@shakyShane that worked, thanks. It would be awesome to see this built in and exposed through the CLI. I'm going to fork and have a play around this weekend!

@markusdosch
Copy link

@thunderkid I had the same problem as you ("worked only for urls that are one level deep") and solved it by adding <base href="/"> to head in my index.html.

I discovered this solution on https://www.bountysource.com/issues/22575141-404-not-found-if-hashbang-false

This was referenced Feb 8, 2016
@mismith
Copy link

mismith commented Mar 8, 2016

@lukejacksonn +1
@shakyShane Is there really no better option for CLI usage than deferring all your config to an external .js file? That seems like a pretty strong argument to bring this functionality into core, or at least for providing some sort of wrapper so that this middleware can be used via CLI / npm scripts

@dnutels
Copy link

dnutels commented Mar 23, 2016

@mismith

Yeah... I am failing to understand that as well.

I, personally, am partial to --app flag @lukejacksonn suggested.

@lassounski
Copy link

You could use connect-modrewrite to solve this.
In your Browserify configuration, require the connect-browserify module, and use it in the middleware option.

var modRewrite  = require('connect-modrewrite');

gulp.task('serve', function () {
    browserSync.init(null, {
        server: {
            baseDir: [APP_PATH],
            middleware: [
                modRewrite([
                    '!\\.\\w+$ /index.html [L]'
                ])
            ]
        }
    });

   // watch only for app specific codes;
   ...
});

@jesusoterogomez
Copy link

I have been experiencing the same error as @thunderkid, with > 1 depth urls.

It actually works okay if I navigate to the links using the application's a tags. But reloading on the page itself, breaks the GET requests to assets again.

For example:

  1. Application starts at /, link to /test/1 is clicked. Everything works fine, css/js assets are loaded from '/`.
  2. Application starts at /. I change the URL to /test/1 manually (or by window.location.href/assign). Assets are attempted to load relatively from /test, making all of them return 404. Same thing happens if I set startPath to /test/1 to make the app start at that specific route.

@jgatjens Do you have a different configuration that the one stated above?

@antonsamper
Copy link

I'm having the same as @thunderkid and @jesusoterogomez and unfortunately i cant use the <base href="/"> trick that @Sukram21 suggested as I'm using using local SVGs and referencing them through the <use></use> tag. Adding the <base href="/"> breaks my SVG references

@markusdosch
Copy link

@antonsamper Probably related: angular/angular.js#8934
This gist (https://gist.github.com/leonderijke/c5cf7c5b2e424c0061d2) should fix the problem for Firefox, its comments still mention problems with Chrome.

@antonsamper
Copy link

@Sukram21 yes definitely related, i came across that a few days ago. It's a shame the the polyfill doesnt work on Chrome

@danukhan
Copy link

danukhan commented Dec 19, 2016

@thunderkid
@jgatjens
I am also having the same issue where using historyApiFallback() is only working for one level. Did you get this issue resolved? Would love to see the solution.
http://localhost:3000/main is working
http://localhost:3000/main/a is NOT working. It just shows in browser saying "connected to browser sync" and then an empty blank page.

@jgatjens
Copy link

@danukhan I remember it was working for all the levels with adamalbrecht solution

@danukhan
Copy link

@jgatjens The issue was that the resource urls (js and css) weren't correctly setup. The following post led me to the answer: http://stackoverflow.com/questions/10865480/mod-rewrite-to-index-html-breaks-relative-paths-for-deep-urls

Basically rather than having my resources as src="res/abc.js", I needed a leading slash like this: src="/res/abc.js"

@danukhan
Copy link

danukhan commented Jan 5, 2017

@jgatjens I'm facing same issue as originally mentioned here while deploying my app in tomcat. i.e., When I type localhost:8080/myapp/page1, I get 404 message. I would like to redirect it to localhost:8080/myapp/index.html as the solution for browserSynch. Do you know how can I do this for tomcat?

@jgatjens
Copy link

jgatjens commented Jan 5, 2017

@danukhan you probably want to use http://tuckey.org/urlrewrite/. In my case, I used the htaccess redirects in prod.

@goldnetonline
Copy link

goldnetonline commented Feb 2, 2017

@lucasmotta come here dude, you need a "wottle of bine". Thanks

@vpezeshkian
Copy link

Did you add a option for this yet? I don't feel like installing an entire module to achieve this, any suggestion?

@xts-velkumars
Copy link

Hi,

I have used gulp to build my client angularJS application. After i deployed into IIS i'm getting an issue. My page is working fine when navigate through menu's. But when i refresh by page (or) copy and paste the URL its giving me 404 error.
HTTP Error 404.0 - Not Found
can anyone help me, to resolved this?

@darlanmendonca
Copy link

@velkumars you problably are with html5 mode enabled, so you need configure your server to redirect all requests to your index.html

look here, has IIS configuration
http://stackoverflow.com/questions/12614072/how-do-i-configure-iis-for-url-rewriting-an-angularjs-application-in-html5-mode

other option, if you cant configure the server, is disable html5 mode, and use a hash in paths, but is a old way :D

@xts-velkumars
Copy link

@darlanmendonca, thank you. i did the same way

@parth-BSP
Copy link

parth-BSP commented Mar 21, 2017

@lucasmotta - this was work like charm in my case..great thank you..

` var defaultFile = "index.html";

// I had to resolve to the previous folder, because this task lives inside a ./tasks folder
// If that's not your case, just use __dirname
var folder = path.resolve(__dirname, "./");
gulp.task('browser-sync', function() {
browserSync.init({
open: true,
port: 80,
server: {
baseDir: "./",
middleware: function(req, res, next) {
var fileName = url.parse(req.url);
fileName = fileName.href.split(fileName.search).join("");
var fileExists = fs.existsSync(folder + fileName);
if (!fileExists && fileName.indexOf("browser-sync-client") < 0) {
req.url = "/" + defaultFile;
}
return next();
}

  }

});`

@Mobe91
Copy link

Mobe91 commented Dec 28, 2017

I discovered that the suggested middleware script has an issue when the url path contains null as in aurelia-templating-resources/null-repeat-strategy.js. The null parts get dropped if fileName.search == null.

Here is an improved version that worked better for me:

function(req, res, next) {
  var fileName = url.parse(req.url).pathname;
  var fileExists = fs.existsSync(folder + fileName);
  if (!fileExists && fileName.indexOf("browser-sync-client") < 0) {
    req.url = "/" + defaultFile;
  }
  return next();
}

@hieutranagi47
Copy link

hieutranagi47 commented Jan 26, 2018

I applied @lucasmotta 's solution and I could solve my case, but I'm facing another issue with "routes" option:

var defaultFile = 'index.html';
var folder = path.resolve(__dirname, './app');
var bowerFolder = path.resolve(__dirname, './bower_components');
browserSync.init({
    open: true,
    port: 8080,
    notify: false,
    minify: false,
    server: {
        baseDir: 'app',
        middleware: function(req, res, next) {
            var fileName = url.parse(req.url);
            fileName = fileName.href.split(fileName.search).join('');
            var fileExists = fs.existsSync(folder + fileName) || fs.existsSync(bowerFolder + fileName);
            if (!fileExists && fileName.indexOf('browser-sync-client') < 0) {
                req.url = '/' + defaultFile;
            }
            return next();
        },
        routes: {
            '/bower_components': 'bower_components'
        }
    }
});

I would like to inject all bower_components into index file to check some configurations in bower.json file for debug instead of merging them into a vendor.js file same as gulp build, and I don't want to store bower_components in app folder, so that, I would like to route bower_components to BrowserSync. but I don't why I can not load the bower file with routes option when I use lucasmotta's middleware.

@rhernandog
Copy link

Hello,

I'm facing a problem when implementing either @lucasmotta or @Mobe91 solutions using React Router and Webpack for deeper routes. Specifically for a route like this: localhost:3000/first/second.

When the browser is refreshed in that route (for code changes or manual refresh) I get the following console errors:

Uncaught SyntaxError: Unexpected token <       manifest.5d6f40b0a76d91d7d841.js:1 
Uncaught SyntaxError: Unexpected token <       vendor.53f4ed0e2a8504e5be36.js:1 
Uncaught SyntaxError: Unexpected token <       app.a815826d5053654ce308.js:1

The browser is actually finding the files, but the issue is that those files are actually the index.html file, so basically javascript is complaining about the less than sign of the opening doctype declaration: <!doctype html>, which is on the first line of the file.

I can actually go to every route in the app, regardless of the url (for example there's no issue when I use a <Link /> to go to /first/second/third), the problem is on reload, the request for each file that returns false is the index.html file.

Using the history api fallback doesn't work neither, in fact it doesn't even get the right url.

This is an example of how the request looks on reload: /lists/js/app.a815826d5053654ce308.js

I'll try with some regular expression to try to get rid of everything that comes before /js and /css and send that request, since normal naivagtion doesn't return false in the fileExists var.

But if someone has some more generic solution (mine would work only for my specific case) it'd be great!!.

@rhernandog
Copy link

rhernandog commented Feb 1, 2018

Hello again,

I found a solution but just for my particular case, but it shouldn't be too hard for everyone to create their own though. Just some regular expressions and it works.

In my specific case I have my compiled files in the /build folder, so that modification has to be made in the folder var:

var folder = path.resolve(__dirname, "./build");

Then, since the issue happens when reloading in a subfolder I created some code to check for more than one forward slash and to see if the request is a file with /css or /js in the filename.

middleware: function (req, res, next) {
  var fileName = url.parse(req.url).pathname;
  var fileExists = fs.existsSync( folder + fileName );
  // test the file name for a .ico file
  var faviconTest = fileName.match( /\/favicon\.ico/g );
  // test the file name for sub-folders
  var subfolderTest = fileName.match( /\//g ).length > 1;
  // test to check if the requested url is a file or not
  var isCssJsFile = fileName.match(/\/css|\/js/g);

  if ( !fileExists && fileName.indexOf("browser-sync-client") < 0 && !faviconTest ) {
    // if the we're in a subfolder and the request is a file (JS|CSS)
    // create a new filename without anything previous to the file folder
    if ( subfolderTest && isCssJsFile ) {
      // create a new file name using the file type
      var newNameRegEx = new RegExp( isCssJsFile[0] + "(.+)", "g" );
      // request the new filename
      var newFileName = fileName.match( newNameRegEx )[0];
      req.url = newFileName;
    } else {
      // we're requesting the base folder so we need the regular index file
      req.url = "/" + defaultFile;
    }
  }
  return next();
}

The code just checks if the folders are there. If you have a different folder structure the code should adapt to that.

Is working fine so far on windows. Finally keep in mind that if you're requesting other files, those folder should also be included. An image's source included directly in the <img> tag doesn't have any trouble.

Best,
Rodrigo.

@sanof3322
Copy link

I've tried Rodrigo's solution with tricking req.url. Even though browser shows no problem, js/css/image files come empty. This is my implementation.

Project file structure:

/app
   index.html
   app.js
   style.css
   libs.js
   /img
      favicon.ico
      site.webmanifest
      favicon-16x16.png

Browsersync init:

const browserSync = require("browser-sync").create();
const url = require('url');
let fs = require("fs");
const path = require("path");
const content_404 = fs.readFileSync(path.join(__dirname, './app/index.html'));
var folder = path.resolve(__dirname, "./app");

const serve = () => {
    browserSync.init({
        logLevel: "silent", //to disable browserSync snippet message
        server: {
            baseDir: "./app",
        },
    },
        (err, bs) => {
            bs.addMiddleware("*", (req, res) => {
                // Provides the 404 content without redirect.

                let fileName = url.parse(req.url).pathname;
                const fileNameArr = fileName.split("/");                
                fileName = fileNameArr[fileNameArr.length - 1];
                const filePath = `${folder}\\${fileName}`;
                const isCssJsFile = fileName.match(/css|js/g);
                const isFavicon = fileName.match(/png|ico|webmanifest/g);
                
                if(isCssJsFile){
                    res.write(fs.readFileSync(path.join(__dirname, `./app/${fileName}`)))
                }else if(isFavicon){
                    req.url = `./app/img/${fileName}`
                    res.write(fs.readFileSync(path.join(__dirname, `./app/img/${fileName}`)))
                }else {
                    res.write(content_404);
                }
                
                res.end();
            });
        }
    );
    
    //watchers go here
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests